iOS: Strategies to transition to Swift Package Manager

Frédéric ADDA
5 min readApr 7, 2022

--

Swift Package Manager

Swift Package Manager (SPM) is the new dependency manager that comes with Swift (since version 3.0). Its main advantage? It’s completely integrated with Xcode. Any iOS / iPadOS / macOS developer can look for and add a new dependency to their project without leaving their IDE.

Swift Package Manager

A number of teams have already transitioned from CocoaPods / Carthage to SPM for its ease of integration and its increased performance.

Concrete case

As an iOS developer at Fiverr, I was tasked to convert our local Networking and Payment libraries (called respectively FiverrNetwork and FiverrPay) from a dev pod (in CocoaPods) to a local package (in SPM).

The creation of the packages themselves had its own challenges, but that was not the main issue. The main issue was the incompatibilities between the new packages and the existing codebase.

Challenges and resolution strategies

1. Same dependency? Same dependency manager.

The 2 newly created packages needed to report non-fatal logs to Firebase / Crashlytics. So that meant adding a dependency from the new local packages to the Firebase package. Fair enough.

To add a dependency to your own package, declare the package to import in “dependencies” and add it to the list of dependencies of the target.

The main app also needed to report to Firebase / Crashlytics. So it made sense to convert the main app dependency from Cocoapods to SPM too. This way, SPM can gracefully resolve dependencies in both the main app and your own packages.
The only issue is that you have to make sure the dependency in the main app and the dependencies in the local packages have the same version / range, to avoid inconsistencies.

Firebase package dependency in the main app

2. Reunite

Instead of having the main app send logs to Crashlytics, and the local packages also send logs to Crashlytics, it made a lot of sense to have only one place sending logs.
We already had another package dedicated to printing logs to the console according to verbosity level, called Logger.
Therefore, why not let our Logger package handle all logs, the local ones (in the console) and the remote ones (to Crashlytics)? This way, only one package has to deal with the correct version of the Firebase package.

In our local packages, the dependencies to Crashlytics then became dependencies to Logger.

A dependency to Crashlytics became a dependency to Logger

3. Conflicting sub-dependencies

A huge library such as Firebase has its own dependencies.
And it can have A LOT.

List of dependencies in the project after importing just the Firebase SPM package

Problems arise when one of our SPM package (Firebase for instance) has a common dependency with another library which is not offered as a SPM package.
In this situation, SPM can’t resolve dependencies because one of the dependencies is actually handled by another dependency manager (in this case, CocoaPods). And you end up having duplicate instances of the same classes(one created by CocoaPods, one created by SPM), which is bound to crash at some point.

That is exactly what happened to us with Google MLKit. We used only a tiny part of MLKit, for instance its Language Identification capabilities, and that tiny part of code was in the way of our SPM migration.

The solution? Find a replacement that is compatible with SPM. This is not easy, and it’s not always possible.
In our case, we replaced Google’s MLKit Language Identification by Apple’s own Natural Language module. This module fit the bill perfectly (even better than the previous one), but more importantly, since it’s provided by Apple natively, it allowed us to remove a source of incompatibility with our SPM implementation.

4. Objective-C strikes back

Some of our legacy code made reference to classes that were included in FiverrNetwork. That was okay until FiverrNetwork was a pod (CocoaPod), but Objective-C classes can’t see the type of objects in a SPM package. Every class in a SPM package is seen in Objective-C as Any.
Therefore the compiler can use it in an Objective-C file, but can’t see its properties.

A class from a SPM package (here, NetworkError) can be used in Objective-C, but not its properties

The solution we implemented? Create a Swift extension to the Objective-C class, and convert to Swift the functions and properties that were referencing the objects in the new package.

Swift extension to the Objective-C class
Now the Objective-C code compiles just fine

Conclusion

Migrating modules from CocoaPods to SPM is not always possible the first time, and sometimes requires migrating smaller components / dependencies first.

However, having only one dependency manager removes:

  • a major source of crashes (caused by duplicate instances),
  • a considerable overhead (caused by maintaining two dependency managers),
  • as well as headaches for the developers (go run pod install and resolve package dependencies each time dependencies change…)

After migrating our own local modules to SPM, and converting as many third-party dependencies to SPM as possible, we are now down to only a handful of CocoaPods dependencies, in line with our objective to transition entirely to Swift Package Manager.

--

--

Frédéric ADDA

iOS developer , born in France 🇫🇷, now living in Israël 🇮🇱. Metal guitarist 🎸🤘. Martial arts practitioner 🇯🇵. Feijoada lover 🇧🇷.