Handle dependencies with Swift Package Manager
For quite a long time, we haven't had anything to manage our depedencies or modularize code in an easy way. When I started iOS development in 2009, if we wanted to use an external library, it had to be included directly into a project. That wasn't the most optimal way to do it.
Then there was CocoaPods. It's a clever manager that helps organize externals for both Objective-C and Swift. All one has to do is create a single file (Podfile) and enter what we need. The downside, at least in my opinion, is that it heavily meddles in project files.
After some time, Carthage came along. It also requires single file (Cartfile), but represents a different approach. The developer gets a bit more flexibility but, again, there are some downsides, i.e., it's hard to switch between versions of a project.
The latest development is Swift Package Manager, which comes with Swift itself. In this case, dependencies, targets, including test targets, are described using Swift language. Command line tool even enables generating whole Xcode projects if necessary.
I assume we start from scratch. Let's create a folder where everything will reside and then initialize the whole structure. Open up Terminal app and type in (or copy if you are lazy).
mkdir Codementor cd Codementor swift package init
The last line creates couples directories and files.
When we open
Package.swift in Xcode, we'll see basic setup. By default, SPM creates a . library, but we can create other types of package with command line switch
switch package init --type executable that creates command line tool setup.
We can simply open each file in Xcode, but that wouldn't be the most effective way of coding, even when we can type in terminal
open [filename]. We can create an Xcode project and use an actual IDE.
swift package generate-xcodeproj open Codementor.xcodeproj
We have to run that command every time we add or remove a new dependency. Xcode will never know that something has changed and xcodeproj file needs to be updated.
Building and testing
If we decide to create an Xcode project file, then we can, of course, build and test the project there. If not, we can rely on command line.
swift build builds the package and
swift test, you guessed it, tests the package. By default, build is done using debug configuration, so to go to production, we need a switch.
swift build -c release does the job.
To complete the whole lifecycle of a project, we need a way to update the dependencies. Other developers work on their projects all the time, and from time to time, we want to get their latest efforts into our codebase. To do so, just type
swift package update. This will fetch the latest versions according to the rules set in
Real life example
Below is an actual package definition taken from my open source project, BinanceAPI.
import PackageDescription // 1 let package = Package( name: "BinanceAPI", // 2 products: [ .library(name: "BinanceAPI", targets: ["BinanceAPI"]) // 3 ], dependencies: [ // 4 .package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", .upToNextMinor(from: "0.8.0")) ], targets: [ // 5 .target( name: "BinanceAPI", dependencies: ["CryptoSwift"]), .testTarget( name: "BinanceAPITests", dependencies: ["BinanceAPI"] ) ] )
Let's go through it.
- We have to import
PackageDescriptionbecause it contains required elements.
- Define the name of the package.
- List products of the package, at least one, but could be more, i.e., command line tool, website, and commons with elements for both.
- List all of the dependencies with version indicators.
- Define targets and dependencies for each and every single one of them.
Why do we do all this? Why do we need another dependency manager?
It's always good to have a choice. We are not forced to use Swift Package Manager — it's one of the options, along with CocoaPods and Carthage.
For me, SPM has one advantage over previous solutions. We can define dependencies within our own project. That means we can say that certain targets depend on other targets and state it in
Before, we also could do that, but everything had to be done "manually" in Xcode. Targets were there, related to each other, but relations were not clear — when somebody new was joining our team, we had to tell them everything about the project.
We don't usually change the project architecture and, sometimes, we just forget things. When all internal and external dependencies are clearly stated in one place, it's easier to remember because we don't have to
Why define internal dependencies?
The simplest explanation would be laziness. We just don't want to repeat ourselves. That approach even got its own abbreviation DRY, Don't Repeat Yourself. When certain code is written in one place, we wouldn't copy and paste it into other place. That would make our codebase unmaintanable after some time.
It's good practice to have code divided into smaller modules. In such cases, we can, for example, define our models in Commons module and import it in both iOS and command line tool.
I learn the fastest when I see an actual usage example. In one of my projects, I have three elements:
Core, where I define all data models.
Parser, command line tool that does some work when run, depends on
Website, presents the data in readable manner, depends on
A whole project is quite simple and might seem like an overkill at first sight, but it's actually very convenient. When a codebase starts to grow, it's still easy enough to move files around and just add
imports here and there, where needed. One thing to remember is to make classes, structs, and everything we want access to, public.
When I wanted to preview the data, there was no need to copy and paste — the models were there, ready to be imported. That can evolve in the future.
Swift Package Manager can definitely help organize projects, even if that's not something really complicated. It's really good practice to start off with modularized codebases. When there is not that many files to move around, it's easier and less troublesome.