The State of iOS Dependency Management

Posted by Rhys Powell on 8 December
Rhys Powell

Dependency management is a tricky problem to solve, even more so on iOS projects where we have to concern ourselves with things like code signing, embedded binaries, and so on. In recent years, a couple of solutions to this problem have presented themselves, each with different approaches and guiding philosophies, and each with their own tradeoffs to consider.

Manual Integration

There's a lot of ways to integrate third party dependencies without using third-party tools. Most OSS projects use Git for source code management these days, so we can include those dependencies using Git submodules. Failing that, we can simply download the latest snapshot of a project and copy it into our project's source tree.

With some basic knowledge of setting up Xcode projects, this solution actually gets us pretty far. It's not the simplest or easiest approach, but it has its advantages. For one, not relying on third-party tools mean other team members can easily build your project without installing additional tools. This method is also the most resistant to breaking changes introduced in newer versions of Xcode, as there's no waiting on a third-party to update their tools.

However we quickly run into issues as we add more dependencies to our project, especially when those themselves also have dependencies. Eventually we're going to run into a case where two dependencies require two different versions of a library, and at that point we have to do a lot of messy manual resolution. Take the following example:

dependency_graph_conflict-1

Here we have a project that uses ReactiveCocoa and the (somewhat less well-known) ReactiveCocoaLayout library. In this situation, both our project and ReactiveCocoaLayout depend on different versions of ReactiveCocoa, so in a manually integrated project we have to do the hard work of resolving this conflict ourselves.

Beause of this, and various other problems, a new solution soon arrived.

CocoaPods

CocoaPods is a Ruby gem that handles the task of resolving dependencies for us, generating an Xcode project that builds those dependencies, and generating an Xcode workspace that lets us integrate them into our project. To include a dependency through CocoaPods, someone has to write a Podspec that describes how to build the framework, and what other dependencies it has. CocoaPods keeps a public repository of these Podspecs for open-source frameworks, and you can specify your own sources for private Pods.

If we wanted to recreate our example from the previous section, we'd create a Podfile in the root of our project with the following contents and then run pod install:

source 'https://github.com/CocoaPods/Specs.git'

platform :ios, '8.0'

pod 'ReactiveCocoa'
pod 'ReactiveCocoaLayout'

This approach is very much inspired by tools like RubyGems, and it has a lot of advantages. For one, it makes integrating dependencies extremely easy by taking over the duty of configuring your Xcode project for you. It also aids in discoverability, with CocoaPods.org providing a searchable repository of open-source pods, and CocoaDocs.org publishing easily searchable documentation.

It's also widely adopted, with most frameworks offering official Podspecs, and those that don't often having community-maintained specs that are kept relatively up to date. This popularity also means that when you do run into issues with CocoaPods, you're rarely the first to and there's likely StackOverflow answers already out there to solve your problem.

For authors of libraries, it can also reduce the maintainence burden of updating projects to build on new versions of Xcode, as CocoaPods handles the entire build process and updates whenever new Xcode versions are released. This does however mean that projects which also want to build without CocoaPods need to specify build steps in multiple places.

However, CocoaPods' ease of use comes at the cost of quite a bit of complexity and a certain loss of flexibility in how you set up your project. For more experienced developers that feel they know how best to set up an Xcode project, the way CocoaPods wrests that control away from you can be frustrating. For library authors, it can also seem redundant to have to create a Podspec that mostly just reiterates information that's already contained in Git and Xcode.

Finally, CocoaPods was initially designed to build static libraries and for a long time lacked any sort of support for frameworks. Initially, this was something only OS X developers really cared about as iOS didn't have framework support prior to iOS 8. However, with iOS 8, not only were frameworks supported, but they became a requirement for using Apple's new Swift programming language. Despite framework support eventually coming to CocoaPods, it has always felt like something of a second-class citizen.

So along came another solution.

Carthage

Carthage is described by its creators as "ruthlessly simple dependency management." Its aim is to act as a simple coordinator between Git and Xcode, that picks compatible versions of dependencies, checks those dependencies out with Git, and then builds frameworks with Xcode.

To recreate our example project with the 'standard' Carthage setup, we would create a Cartfile at the root of our project with the following contents and then run carthage bootstrap:

github "ReactiveCocoa/ReactiveCocoaLayout"
github "ReactiveCocoa/ReactiveCocoa"

This would pull down our dependencies and build them as frameworks, but we would still need to manually integrate them. To do this we'd simply add references to them in our Xcode project and add them to the linked frameworks section of our target's build settings.

Compared to CocoaPods, it's a much simpler tool, though perhaps not as easy to use. Carthage does not integrate dependencies into your project in the same way CocoaPods does, instead leaving that task up to the developer. The upside of this is increased flexibility in how you integrate dependencies: whether by embedding the built frameworks, or by creating a workspace that includes all the cloned Xcode projects of your dependencies. In fact, Carthage can add all your dependencies as Git submodules, removing the need for other devs to install Carthage at all.

For library authors, supporting Carthage couldn't be easier. You simply need to make an Xcode project that can build your framework, and then share the build schemes contained in that project. Compared to CocoaPods' spec-based approach, this is refreshingly simple. There's no need to submit a spec to any sort of central repository, instead users simply refer to a GitHub repo or Git URL in their Cartfile. This decentralised approach also simplifies the use of private frameworks and forks.

Carthage also tends to speed up builds in a few key ways. Firstly, frameworks are built once when you bootstrap or update your project, and then simply embedded on subsequent builds. Secondly, Carthage integrates with GitHub's Realeases feature to download precompiled frameworks wherever possible.

Compared to CocoaPods, Carthage doesn't have quite as much traction. There's still the occasional project out there that doesn't expose any shared build schemes, but this is usually simple to resolve with a quick pull request. Carthage also doesn't help with discovering new libraries to use, your best bet there is to search GitHub.

Swift Package Manager

Announced as part of Swift going open-source, the Swift Package Manager, or swiftpm, marks the first time we've had something akin to an official dependency manager. It's perhaps a little soon to say too much about swiftpm, it's still very early and not officially released, but the current implementation and the package manager community proposal provide a lot of food for thought.

With swiftpm, developers create a Package.swift file in the root of their project that describes their project and its dependencies. swiftpm then resolves those dependencies based on git tags, pulls them down, and builds them as static libraries.

The Swift Package Manager is similar in certain aspects to both Carthage and CocoaPods. Like Carthage, it uses a decentralised approach to dependency resolution, and builds those dependencies as standalone binaries. Like CocoaPods, it uses specification files to define libraries and their dependencies — though this perhaps makes more sense for swiftpm, as it has to run on platforms where Xcode and by extension xcodebuild don't exist.

As swiftpm builds static libraries as opposed to frameworks, there's currently no way to include resources like XIBs or image files in a library. However, this is highlighted by the fairly in-depth package manager community proposal as something to add in future releases, alongside numerous other features that would make it more feasible for iOS projects.

In fact, looking at the proposals set forth in that document, it seems clear that the Swift Package Manager will eventually become the de-facto solution for dependency management on iOS projects. Just not today.

Which One Should You Use?

As with most things, the answer is 'it depends'. If you want to easily bootstrap a new project, CocoaPods will take care of most of the work for you. If you want more flexibility in how you set up your project, or you want to more easily use private libraries that aren't in a central repo, Carthage is probably the better option.

New Call-to-action

Topics: Development, Mobile, Swift, Apple