Finally, Apple native package manager is available on iOS. It’s called Swift package manager and its role is to replace Cocoapods and Cartage.
In the upcoming series of posts, we will discover the world of Swift Package Manager. On the first try, we will focus on the theory behind it. Let’s start!
What is SPM
The Swift package manager is integrated with Swift build system for managing code distribution. It allows automating the process of downloading, compiling and linking dependencies, making import of libraries into application very easy.
Swift package manager (SPM) had been available since Swift 3.0, but until Xcode 11 was released, it has been mostly used for a server-side application, due to not being supported on all of Apple’s platforms. That being said Xcode 11 brings SPM to another level – official support on each platform and full integration into Xcode itself will make it valuable dependency manager for Swift code.
Swift is all about modularization. It organizes its code into modules, packages, products and dependencies. Let’s briefly check what each of those words really means.
Module is the main tool in SPM workshop and it serves as a single unit of code distribution. It has to provide specific namespace along with access control on its parts of code. These parts are called source files, which relates to one single file of Swift code within a module. In Xcode terms module is separate build target (either app bundle or framework).
Swift is all about modularization and modules are all about reusability. We create modules to share code between different apps or other modules. The developer can include one module into another using keyword import. For example, you probably have imported UIKIT on daily basis to get UI components into the app. Another example could be also Alamofire famous library for elegant network requests in Swift. By using it you don’t have to bother with creating your own wrapper for networking. Such an approach saves us, developers, enormous amounts of time, since we don’t need to waste it on inventing the wheel again – you need some generic functionality? Maybe there is already a module for that available, and you can focus on your app-specific behaviours.
You may start to wonder when code should be extracted to separate module. Unfortunately, there is no golden rule for that, you have to lean on your instinct and common sense, but general rule coming from official docs is: „more modules is probably better than fewer modules”.
Package contains source files and a manifest file, called Package.swift which describes the package. We will discuss manifest later. The single package consists of at least one target, which specifies a product and may define dependencies.
Target build either library or executable as its product. The library is a module that could be imported and the executable is a program that could be run.
Dependency is module required by package and it is specified in Package.swift file. It needs to consist of URL, either relative or absolute, and also it needs to have specified version or requirements for resolving the correct version. The core role of SPM is to automate downloading and building dependencies and it is mostly focused on reducing coordination cost of these.
Dependency graph is a dependency with its own dependencies. The manager tries to resolve the graph and downloads, builds each part of it.
In application with multiple packages possible issue is „dependency hell”. This colloquialism means that dependency graph cannot be resolved. Common scenarios can be:
- Wrong versioning – the package was released with breaking changes, but with minor/patch tag (more on versioning later)
- Incompatible versions requirements – two or more packages depend on the same package but for different versions of it. It could happen for both minor and major requirements
- Namespace collision – two or more dependencies have the same name
- Broken software – the package has a dependency with a bug, which is impacting the whole app
- Global State conflict – two or more dependencies are referring to the same global state at the same time
- Unavailable – one or more dependencies are unavailable (change of URL, deleting published version etc)
The package manager is designed to minimize the risk of the above scenarios and to provide user tools to deal with „dependency hell” when it happens with a minimum of trouble.
SPM uses semantic versioning as a tool for dealing with „dependency hell”. It is a set of rules and requirements that dictate how package version numbers are assigned and incremented. Some basic rules are:
- Package must have public API
- Format of version numbers is X.Y.Z (major.minor.patch). Patch version is bug fix without updating API, minor is an API update with non-breaking changes, major are breaking changes.
- The version number must not contain leading zeros or be negative.
- Package with 0.Y.Z is considered not stable and used for the initial development
More about semantic versioning can be found here.
Let’s check the theory with an example!
Our example is an unfinished App which simulates Blackjack casino game. Here is a link for the repository.
After downloading repo and opening project in Xcode, we can check Swift Package Dependencies section, which consists of two items: Cards and Swift Format packages.
So what are these two?
Cards is a library which defines a Card, Deck and CardView which all will be used in our app. Here is a link to its repo.
As we can see Cards is resolved at version 1.2.0 which means that it has stable API and from first stable version there were two API updates. If we would check commits and tags, we would have seen that:
- Version 1.0.0 contained Card only
- Version 1.1.0 adds Deck
- Version 1.2.0 adds CardView
Swift Format is dependency added by Cards – it is an open-source tool for code formatting, you can check it right here.
As the last thing for versions: we can see that our Blackjack App is 0.1.0 version. Why? Well if we would run our app we would have seen that game is far from being finished!
Now let’s open project settings and go to Swift Packages, where we see only Cards package with version rule: 1.1.0 – up next to major. So why Swift Format is present? Simply because being Cards dependency it’s added in Cards Package.swift file.
So let’s dig into that file.
Firstly we see the package name definition. Then we see platforms for which we want to develop our package (here is just the latest iOS version). Next thing is the product section and there we see that our Cards is a library, so it cannot be executed/ran by itself (except tests). The fourth section is „dependencies”, where we see Swift Format with URL to repository and version specifier: from „0.35.8” to next major. Last two sections are „targets” and swift language version (Swift 5 + ). Our package produces one target (Cards) and test target for it (CardsTests).
In this post, we learned a few core things that rule Swift Package Manager world. What next, you may ask?
Firstly we could give our app a better UI: maybe instead displaying pure Rank and Suit of card, we could display an image of a given card?
Or maybe we could develop a package, so more players could be involved into play?
But how to update or add new package? That will come in the next post! See you there!