As of Xcode 12, Apple has matured Swift Package Manger to a degree where it makes sense to add support for Swift packages to your libraries. There are still a few stumbling stones on the path which have no obvious solution. So I figure, I’d share with you how I got around them when I recently added SPM support to DTCoreText, DTFoundation and Kvitto.
Before SwiftPM, my general approach for a library would be to have all library code in a `Core` subfolder, with a `Source` folder containing code which gets compiled and a Resources folder for all kinds of resources, like for example asset catalogs or XIB files.
A Bit of History
For the first 7 iOS versions the product of this product could only be a static library, Apple only introduced the ability to create dynamic frameworks for Objective-C as of iOS 8. With Swift it was the other way around: you could only have dynamic frameworks with Swift code. For the first 4 versions of Swift the ABI (Application Binary Interface) was too much in flux to allow a statically linked product. With Swift 5, in 2019, we finally got the required stability and thus Xcode gained the ability to produce static libraries containing Swift code. This is also the main reason why Xcode always added a bunch of dylibs to your apps, containing Swift wrappers to all the frameworks your app might be interfacing. Those dynamic libraries are the third kind of libraries we have encountered so far.
Oh boy, I remember all the hackery we had to do to produce a „fake“ framework that was essentially a fat static library (with slices for all supported processors) and all public headers. We would that so that somebody using our library could drop it easily into their project and have all exposed interfaces be visible. In Objective-C you would need to have the header files available for public functions and classes contained in the library. Those `.framework` bundles provided a nice encapsulation of that, so that it was almost like handling a single package adding a third-party framework to your app.
Dynamic frameworks – in real life, on device – actually don’t contain any headers any more as those become useless after compiling. The main benefit of first-party dynamic frameworks is that Apple can have their APIs and code shared between all apps installed on the device. The one and only UIKit framework – installed as part of iOS – is being accessed by and dynamically linked to all installed iOS apps. Only a single instance is present in RAM at any time. Custom frameworks cannot be shared between multiple apps due to all apps being contained in their own little sandbox. Every iOS app containing DTCoreText for example has to have its unique copy of it inside its app bundle. If an app has a great deal of third-party frameworks that process of loading all frameworks into memory and dynamically linking can noticeably slow down app launch.
Swift Never Had Headers
With the innovations brought with Swift also added the concept of modules to Xcode. The Swift Programming Language Website offers this definition of modules.
A module is a single unit of code distribution—a framework or application that is built and shipped as a single unit and that can be imported by another module with Swift’s import keyword. Each build target (such as an app bundle or framework) in Xcode is treated as a separate module in Swift.
When you import a module in your code, then Xcode somehow magically knows all about the public interfaces contained in it, without ever having to have a separate header file. I don’t know how exactly that works, but I am glad that it does!
It was the problem of discovering and integrating third-party libraries into your codebase, that Cocoapods was invented to solve. The first public release of it was almost exactly 9 years ago, in September 2011. With the default settings – not using frameworks – Cocoapods would compile the third-party code and merge it with your own, resulting in a single monolithic app binary. And of course it would manage all those Objective-C headers for you. If you added
use_frameworks! to your Podfile then the strategy would change to instead create a framework/module per pod/library. And that would be the requirement for when you were using external libraries written in Swift, or so I thought …
I’ve always used that in apps I am working on which use Cocoapods for dependencies. Imagine me rambling on to a client of mine about the disadvantages of dynamic frameworks, trying to convince him of the benefits of Swift Package Manager. Imagine my surprise when we inspected his app’s bundle, only to find but a single framework in there. All the third party code he had ended up fused with the app binary, my library – written in Swift and integrated via git submodule and Xcode sub project – resulting in the only dynamic framework in his app.
By default, CocoaPods had been doing all along what we know to be the smarter choice: if third party code is available, to merge the object code it into the app binary. Of course closed-source frameworks which are only available as dynamic framework binaries leave you without this option. Personally I try to avoid those, like the devil avoids holy water.
Oh and I also will be the first to admit that I could never warm myself to Carthage. I have never looked at it. As far as I understand, the difference in approach versus CocoaPods is that Carthage only needs a repo URL to add a component, whereas CocoaPods needs a Podspec and will generate an Xcode workspace for you where all dependencies are set up in a Pods project. I believe it might be this workspace wizardry that might put some people off Cocoapods.
Resourceful Swift Packages
Before the current version 5.3 of SPM the two big remaining pain points have been the lack of handling of resources and no support for distributing binaries as packages. Those have now been remedied and what’s the best part is that Swift packages now have proper integration in Xcode 12.
Another big advantage that CocoaPods had over other dependency managers was the existence of the “trunk”, a centralised repository of available pods. There you could search and find libraries that would fulfil certain needs of yours. Another important aspect would be that for a version to be released on the CocoaPods trunk, you would have to “lint” your pod spec which would validate the syntax and make sure that the library builds without errors or warnings.
Apple (and the SwiftPM open source community) have worked on polishing the tool itself. But the central repository with validation aspect of package management was unfilled. Until Dave Verver stepped and established the Swift Package Index. In his own words:
The Swift Package Index is a search engine for packages that support the Swift Package Manager.
But this site isn’t simply a search tool. Choosing the right dependencies is about more than just finding code that does what you need. Are the libraries you’re choosing well maintained? How long have they been in development? Are they well tested? Picking high-quality packages is hard, and the Swift Package Index helps you make better decisions about your dependencies.
It was this implementation of a central index, focussing on package quality, that pushed me over the edge to finally start embracing SPM. With CocoaPods it has been a tedium to set up a CI server to keep building your libraries for every change to make sure that nothing breaks. By contrast, SPI builds your package with Swift versions 4.0, 5.0, 5.1, 5.2, 5.3 for iOS, macOS Intel, macOS ARM, Linux, tvOS and watchOS and will then show on the package’s page where that worked.
This page gives a very nice overview by which developers can gain an idea as to the quality of this library. And for us project owners it provides an incentive to try to maximise the number of green checkmarks you see.
SPI still tracks 5.3 as “beta” although Xcode 12 has gone gold a month ago. The reason being that Apple has rushed out Xcode 12 and the finalised support for building universal apps that can also run on Apple Silicon will be in Xcode 12.2 – available later this year.
I also like how SPI tracks both the latest stable release (via tag on master) as well as the progress on the develop branch. I wished for those builds to be coming sooner, ideally right after pushing changes to the GitHub repo, but sometimes it can take a long time for the builds to be scheduled. Also a way to retry a failed build would be very nice, as we are used to from Travis-CI or GitLab-CI.
At this point I wanted to go into the things I learned so far from adding SPM to some of my libraries, but I am still fighting with SPI over some of those coveted checkmarks. Also this article has already turned out longer than I wanted it to, that I’ll do that in the next one.
Let me know if that is of interest to you, by dropping me a tweet. Are you considering adding SPM yourself? Which part did you struggle with?
Part 2 is here.
Also published on Medium.