Swift 101: Building a Library with Swift Package Manager
The Swift Package Manager, or SwiftPM, is included with Swift 3.0 and above. Initially, it was only available for server-side or command line Swift projects. Since the release of Swift 5 and Xcode 11, SwiftPM is compatible with the Apple ecosystem for creating apps. This is great news because packages make it easier to divide your code into reusable, logical groups you can easily share across projects, or even with the entire world.
📦️ Modules
Before looking at packages, we first need to understand modules. Swift organises code into modules. Each module defines a namespace and which parts of the code can be used from outside the module. You can define all of your code in a single module, or break it up into multiple which can depend on each other. Using modules lets you easily build on your own re-usable code, or others code.
🎁 Packages
So what is a Swift package? A package is a collection of Swift source code files as well as a manifest file called Package.swift
, that defines various properties about the package, such as its name, the products it produces, any dependencies it has, and the targets it is built up of.
🦴 Anatomy of a Package
- Products define the libraries and executables produced by a package. A library is simply a collection of files, for use as a dependency by other Swift code. An executable is a package that can be run, such a web server.
- Dependencies are other Swift Packages you want to use code from, within your package.
- Targets are what defines the module(s) that a package contains and that other packages can import. Targets define their own dependencies and can depend on other targets the package, or products from packages that this package depends on.
⚙️ Creating a Swift Package
This tutorial assumes you already have Swift installed. You can check by running
swift --help
from your terminal.
Creating a Swift Package from the command line is easy, and can be completed with one simple command from the directory you want to create your package in. For this example we’ll start with a directory named FooPackage
.
$ mkdir FooPackage
$ cd FooPackageFooPackage$ swift package init --type=library
That’s it! There’ll be some output detailing the files created for your new package. You should see:
- 1 source file created inside a
Sources
directory - 1 test source file inside a
Tests
directory - A
Package.swift
manifest file at the root level - A README.md file at the root level
- A .gitignore file at the root level
Of these files, only the single file in the Sources
directory and the manifest file are required for the package to build. This means you could easily create your own package by manually creating these two files as well.
By default, the Sources
directory must contain all source code for the package, but you can use sub-directories to define sub-modules, if they are also defined as separate targets in your manifest file. Let's take a look at the generated Package.swift
for the new package to see the pieces we've discussed so far:
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.import PackageDescriptionlet package = Package(
name: "TestPackage",
products: [
.library(
name: "FooPackage",
targets: ["FooPackage"]),
],
dependencies: [
],
targets: [
.target(
name: "FooPackage",
dependencies: [
]
)
.testTarget(
name: "FooPackageTests",
dependencies: [
"FooPackage"
]
)
]
)
Here you can see that our package defines one library, TestPackage
, as well as one target of the same name, and one test target, with depends on the module target.
🥇 The first build
Now that the package has been created, let’s build it for the first time with the build command:
$ swift build
Because the package has no dependencies or code yet, this should complete almost instantly, displaying “Build Completed!” on success.
➕ Adding dependencies
Let’s add a dependency and some code. Adding dependencies with SwiftPM is easy as you can use git URL’s directly. We can add the following to our Package.swift
top level dependencies block to allow us to the Appwrite Swift SDK in our library:
.package(name: "Appwrite", url: "https://github.com/appwrite/sdk-for-swift", from: "0.1.0")
This declares that our package will pull in the code from the Appwrite
module in the sdk-for-swift
repository on GitHub, from the tag 0.1.0
and allow us to add it to our targets dependencies as follows:
.target(
name: "FooPackage",
dependencies: [
"Appwrite"
]
)
Here we added Appwrite
, as this is the name of the library we're using from the sdk-for-swift
repository.
Let’s take a look at the full manifest file with the new dependency added:
// swift-tools-version:5.3
// The swift-tools-version declares the minimum version of Swift required to build this package.import PackageDescriptionlet package = Package(
name: "FooPackage",
products: [
.library(
name: "FooPackage",
targets: ["FooPackage"]),
],
dependencies: [
.package(name: "Appwrite", url: "https://github.com/appwrite/sdk-for-swift", from: "0.1.0")
],
targets: [
.target(
name: "FooPackage",
dependencies: [
"Appwrite"
]
)
.testTarget(
name: "FooPackageTests",
dependencies: [
"FooPackage"
]
)
]
)
Since we’ve changed the dependencies of our package, we need to resolve them. This will happen automatically the first time you run swift build
with a new dependency, but if you manually update a version, you'll need to manually resolve the new version. This can be done by running:
$ swift package resolve
This will update the Package.resolved
to contain the version metadata about the Appwrite
module we just added.
What’s going on here?
Swift Package Manager uses a lockfile system similar to
package.lock
for NPM andcomposer.lock
for Composer. This comes in the form of a file calledPackage.resolved
, which contains metadata about the packages dependencies versions, as well as their transitive dependencies. When you runswift build
and the dependencies are fetched, the versions from thePackage.resolved
file will be used if found.
Once resolved, we can build our package with swift build
again. This time we'll see the sdk-for-swift
repository pulled into the build checkouts, as well as built with the rest of the library.
📥️ Adding library code
Time to add some code. Let’s open up the source file created earlier as Sources/FooPackage/FooPackage.swift
and update with the following:
import Appwritestruct FooPackage { static let client = Client()
static let account = Account(client) public static func login(
endpoint: String,
projectId: String,
email: String,
password: String,
completion: @escaping (Result<Session, AppwriteError>) -> Void
) {
client
.setEndpoint(endpoint)
.setProject(projectId) account.createSession(
email: email,
password: password,
completion: completion
)
}
}
We now have a login function! We just need to deploy the package, and we’ll be able to use the login function from any other package or Apple app.
🧑💻 Deploying the package
Fortunately deploying packages with Swift Package Manager is very easy. As the packages are Git based, all you need to do is push your changes to your default branch and create a tag for your release:
$ git init
$ git add .
$ git remote add origin [GitHub Repository URL]
$ git commit -m "Initial Commit"
$ git tag 1.0.0
$ git push origin main --tags
📥️ Using as a dependency
Using the same method we used earlier to add the Appwrite Apple SDK as a dependency, we can now add the newly deployed package as a dependency of a second package:
... dependencies: [
.package(name: "FooPackage", url: "https://github.com/[YOUR GITHUB USERNAME]/[YOUR GITHUB REPOSITORY]", from: "1.0.0")
],
targets: [
.target(
name: "FooPackage",
dependencies: [
"FooPackage"
]
)
] ...)
🏗️ Using the dependency
With the package added as a dependency, we can now use the function we defined earlier anywhere in the second package:
import FooPackageFooPackage.login(
endpoint: "http://localhost/v1",
projectId: "6bfgh45fng3",
email: "test@test.test",
password: "password"
) { result in
...
}
🔽 Updating your package
To update your package, the process is the same as deploying the initial version. You just need to push your changes to the default branch and a new version tag to go with.
✅ That’s it!
You’ve now created, deployed, used and updated your very own Swift Package! Packages are a great way to re-use code and share your creations with the world. Looking forward to seeing what new packages come next for Swift!