Get Started with Swift Package Manager (SPM)
Managing dependencies and 3rd-party libraries was never easier. With Swift Package Manager (SPM), you can add external libraries to your iOS project. It’s built right into Xcode, which is super convenient.
In this tutorial, we’ll discuss how to manage dependencies with SPM. Here’s what we’ll get into:
- How to add dependencies to your iOS project wit Swift Package Manager
- What’s Swift Package Manager and why is it a useful tool?
- How semantic versioning works, and what problems it solves
- Pitfalls when dealing with library versions, like breaking changes
- How to manage dependencies with Xcode in real-world apps
Ready? Let’s go.
- What’s Swift Package Manager?
- Adding Dependencies to Your App
- How Semantic Versioning Works
- Managing Dependency Versions with SPM
- Further Reading
What’s Swift Package Manager?
Swift Package Manager (SPM) a tool that you use to manage dependencies in your iOS app projects. A dependency is a 1st- or 3rd-party library, package or framework. For example, you can add Alamofire to your iOS project with SPM.
Swift Package Manager is officially supported in Xcode, because it was created by Apple and the Swift.org team. SPM has been part of Swift since it was open sourced, but it took until Xcode 11 for SPM to get integrated in the Xcode IDE.
Alternatives for Swift Package Manager are CocoaPods and Carthage. In fact, CocoaPods – which is independently developed – has been the default dependency manager on iOS for a decade. It’s a great tool for production apps.
Before we move on to discuss how you can get started with Swift Package Manager, let’s briefly take a look at what a dependency exactly is.
A dependency is a library, framework, package or SDK that your app uses. It’s often 3rd-party code that you add to your iOS project. Many dependencies, like Alamofire, SDWebImage or RxSwift, are open source. You can freely read, modify and download the source code (depending on their license).
This is an obvious advantage, because you don’t have to write that code yourself. Open source is built on the principle that you can use and/or contribute to a shared library or framework. Alamofire, for example, can help you to make HTTP networking requests.
You don’t have to limit yourself to external, 3rd-party dependencies. You can also use Swift Package Manager to manage internal dependencies, like a library or framework that’s developed by your in-house team and deployed in several apps. In this tutorial, we’ll focus on 3rd-party code – but this guide is helpful for adding local packages with SPM.
Let’s dive into SPM and Xcode!
Note: Adding private/internal dependencies is straightforward. You can add your private GitHub, GitLab and BitBucket accounts to Xcode via. You then add the repository URL in the same way as before, and authentication with your account should happen automatically. Configuring the dependency’s package is more complex, though.
Adding Dependencies to Your App
Let’s discuss how you can manage dependencies with Swift Package Manager (SPM) in Xcode. It’s quite easy, actually.
First, make sure you’ve created a new project in Xcode or opened a project of your own. We’re going to add one of these popular libraries with SPM:
You’ll notice that these URLs point to GitHub, which is where you’ll find many open source Swift and iOS libraries these days. Each of these repositories has a
Swift.package file, which gives Swift Package Manager more information about the library.
Next, go to themenu in Xcode. You’ll see this dialog pop up:
We’re going to input a URL to a Swift package repository, in the search field you see in the above screenshot. This can be just about any URL, as long as it points to a repository that contains a
Package.swift file. Alamofire is a good choice – let’s put in that one. Then, click Next.
Xcode will now verify the repository and present you with the dialog below:
You can choose between these 3 options:
Version: This allows you to specify a version of the library, like
5.4.3. This is the recommended option. We’ll discuss semantic versioning in the next section. The latest release version is usually preset already.
- Branch: This lets you select and add a specific Git branch of the library, to add to your project. Some published libraries have multiple branches, for example for new Swift versions or pre-release features.
- Commit: This lets you select a commit from the Git repository, from the development version of the library. It gives you the greatest control over what “version” of the library you’ll get. Useful dev phase apps.
When you choose Version, you can choose between Up to Next Major, Up to Next Minor, Range and Exact versions. We’ll pick Next Major for now, and click Next.
Xcode now downloads the code from the Git repository. The following dialog appears:
A Swift Package Manager package, like the Alamofire library, can produce multiple products. They are the “output” of a package, i.e. the compiled library or executable (“build artefact”) that can be used in an Xcode project.
In the above screenshot we’re only seeing one. The Realm library for iOS, for example, has 2 products:
Realm (core) and
RealmSwift. You’ll have to consult the documentation of the dependency you’re trying to add, to figure out which products you’ll need to add to your project.
The Add to Target column indicates which of the products is added to the targets your Xcode project defines. In most cases, you’ll have 1-2 build targets for your app. Make sure that the checkbox is ticked, and then click Finish.
Finally, the following screen appears:
In the above screenshot, you can see that we’ve added the Alamofire library to our Xcode project. You can reach the above screen by clicking on your project in the Project Navigator on the left, then choose the item below Project (i.e, your project) and finally go to the Swift Packages tab.
You can verify a few pieces of information on-screen:
- On the left, in the Project Navigator, you see a list of dependencies that have been added to your app. You can actually drill-down into the source code of the library here!
- In the table in the middle, you can see the version information of the dependency, as well as the URL it’s coming from. You can use the + button to add another dependency to your project.
Let’s get a move on, and discuss how versioning works!
Pro tip: You can use a library in a playground, if you add that library as a dependency to a project, then add the playground to the project too, and then add an
import module at the top of the playground. Neat!
How Semantic Versioning Works
Semantic versioning (semver) is a widely used standard to figure out what versions of software you have, and can adopt in your own projects and tools. It solves (part of) the problems that happen when you need to keep 2 software projects compatible.
Before we discuss the nitty-gritty, let’s find out what happens when a library you’re using in your app releases a new version. For example, Alamofire releases a new version 6 of the framework – and you’ve used v4 or v5 in your own project. How do you know if the new version is compatible with your app?
You’re using code that was written by another coder or company. They’re fixing bugs, adding new features, and occasionally, these changes will break your integration with that code. A function has been renamed, for example.
If you were to download that incompatible version into your project, and build your app, you’d get a bunch of errors – and that sucks. Or worse, the app builds OK, but it introduces bugs down the line.
To solve this problem, software developers (and package maintainers) use semantic versioning. It’s a version numbering system we all agree on, with a few preset rules around what is a “next version” and what is not.
Note: Semantic versioning isn’t just used to give libraries and frameworks a version number; it’s used for software, CLI tools, operating systems, and everything in between.
It’s all you need to remember:
x.y.z. In the above screenshots, for example, the Alamofire library has version 5.4.3. That is…
- … major version 5
- … minor version 4
- … patch 3
Counting happens naturally, so it could happen that after some time of fixing bugs, we’re at Alamofire 5.4.6 or 5.4.10. Here’s what constitutes a major, minor or patch update:
- A major update happens when you make changes to an API that are incompatible with previous versions. This usually means major breaking changes (and major new features!).
- A minor update involves new functionality in a backwards-compatible manner. This means that the new version of the library is compatible with older versions (“backward”). They (usually) don’t break.
- A patch update is similar, except that it’s reserved for bug fixes. You can typically update from
You can imagine semantic versioning as layers of an onion. Like this:
Changing the core of the onion – the major version number – will affect everything outside of it. Merely make a patch, and it won’t affect the integration at all. Neat, right? It’s an intuitive way to determine compatibility – and effort required – of a new dependency version.
Note: Version numbers are positive integers from 0 to n. Zeroes on the right are insignificant, so technically 5.2 is equal to 5.2.0. You’ll see both variants in real-world coding.
Managing Dependency Versions with SPM
You can imagine that, based on semantic versioning, it would be helpful to select which versions of a library you want. And that’s exactly what Swift Package Manager, and dependency managers like it, can help with!
In short, you can use a rule to specify which new versions of a dependency you want. This rule is usually something like “Up to major version 6, but not including”. That would give you library versions from 5.0.0 to 5.9.9, so to speak, but not 6.0.0. (Actual numbers can go above 9, though!)
Quick Tip: You can ping Xcode to update to the latest (specified) version of a dependency via themenu. Keep in mind that the dependency itself may also have dependencies, i.e. you may not be able to update to a new version of a library if that library requires, say, iOS 15 and you’re on version 12.
When we added this library to Xcode, via Swift Package Manager, we could choose between a few versioning options. They are:
- Up to Next Major: Any versions up to the next major version, i.e. 5.0.0 to 5.9.9, but not 6.0.0.
- Up to Next Minor: Any versions up to the next minor version, i.e. 5.1.0 to 5.1.9, but not 5.2.0.
Range: Lets you specify exactly which versions you want, with the less than
<operator. It lets you put a limit on the highest version you want to support.
- Exact: Pick one exact version – no automatic versioning or upgrades.
A common choice is Up to Next Major, because minor version updates shouldn’t introduce code-breaking changes. You can also be more conservative, and opt for Up to Next Minor.
Even though semantic version looks super strict, real-world software development is a bit more “flexible”.
A common problem with dependency management is that you have no formal guarantees that my 5.4.3 is the same as your 5.4.3. This is why many software developers choose to commit dependencies to a project’s Git repository as well, even though that code could be downloaded from the library’s Git repository come build time.
Similarly, although many library maintainers and software developers give it their best, it could always happen that a change that’s supposed to be non-breaking (i.e., a bugfix) ends up breaking your code. It’s not unthinkable that a bug fix might requires you to approach an API in a different way, especially early on in development when a library isn’t stable yet.
In any case, it’s smart to keep an eye on the release notes of a new library version. You’ll find that it’s often easiest to “pin” to a certain version of a library, and to periodically patch and upgrade to newer versions. Combine that with debugging and testing, and you’re certain your app won’t crash in production!
Swift Package Manager is an exceptionally convenient way to manage dependencies in your iOS app projects, and it’s built right into Xcode. Even better!
In this tutorial, we’ve discussed how you can use SPM to manage dependencies. You’ve learned how to add a library to your app, and how to specify the versions you want to download. We’ve also discussed how semantic versioning works, and how you can avoid common pitfalls with dependency management.
Want to learn more? Check out these resources:
Code Swift right in your browser!
Go to the Swift Sandbox