These days, developers that write all their own code are rare, and with good reason. Why reinvent the wheel when you can find well written, and well supported, third-party libraries that will help build products faster? In fact, we just wrote an article describing our own process for evaluating third-party libraries.
Inserting other people’s code into your project does throw up some issues, including the need to effectively manage all of these various libraries. A role commonly filled by dependency managers.
What is a Dependency Manager?
A dependency manager is simply a tool that helps you integrate external libraries into your project. Usually, they include a list that states what libraries and versions are available and how to grab them. Once the dependency manager knows all of this, it takes care of fetching the correct versions and integrating that into your project (or leaving the libraries ready to be integrated by you at a later date).
In this article, I will focus on dependency managers used within the Apple environment: Carthage, CocoaPods, and Swift Package Manager.
This post is not a beginner’s guide to dependency managers, it is instead a cross-reference so developers can easily learn to switch between CocoaPods and Carthage. For example, let’s suppose you have worked extensively with CocoaPods, but suddenly find yourself on a new project that uses Carthage. You don’t need to learn what Carthage is – you just need to learn how to perform the actions you previously did in Cocoapods, but on Carthage. This article will facilitate this, increasingly common, transition. I’ve also sketched out an introduction to the newer Swift Package Manager to highlight its current use cases and weaknesses.
CocoaPods for Carthage Users
Launched in 2011, CocoaPods was the first dependency manager for the Cocoa and Cocoa Touch environment. As it is written in Ruby, it has to be distributed as a Gem. So you will need a compatible version of the Ruby language for CocoaPods. At the moment of writing this, the cocoapods-core gem requires Ruby 2.0.0 or later (macOS 10.13.3 ships with Ruby 2.3.3 so that shouldn’t be a problem).
CocoaPods has a centralized approach. This means that CocoaPods has a repository (on Github) that maps the name of a Pod (the name libraries receives on CocoaPods) with the location of where to download that Pod. The advantage of having a centralized approach is that you have a place where you can store the metadata of all the available libraries on CocoaPods. In turn, this provides a simple way to search for libraries. The drawback is that if something happens to that repository you won’t be able to resolve your dependencies.
In Carthage, when you run
carthage update, the program checks out the dependencies, builds them and leaves them ready for you to grab and integrate them into your project. CocoaPods is different, when you run
pod install, it will download the libraries and create a new .xcworkspace. This generated .xcworkspace will include two projects within it: the project you just created, and another one called ‘pods’. From now on, you will have to use this workspace to develop your app. You can check out the screenshot below to get a bit more clarity on this.
When you compile your project the dependencies are also compiled. CocoaPods configured everything just by running
pod install. Nice!
To indicate which libraries you want to use in your project you need to add them to the Podfile. This is a ruby file where you can specify the pods, versions, and targets are to be installed. Also, the
pod install command will generate a Podfile.lock file that is designed to keep track of the installed versions of your libraries. This is similar in concept to Carthage’s Cartfile.resolved file.
Her is what a Podfile looks like:
platform :ios, '9.0' inhibit_all_warnings! target 'MyApp' do pod 'ObjectiveSugar', '~> 0.5' target "MyAppTests" do inherit! :search_paths pod 'OCMock', '~> 2.0.1' end end
As you can see, the syntax to define a pod is the following:
pod 'PodName, 'Version'
Both Carthage and CocoaPods use Semantic Versioning, so the ways of specifying a version are:
>= 1.0At least version 1.0~> 1.0Compatible with version 1.0== 1.0Exactly Version 1.0
You can also specify a commit, a tag or a branch to use:
pod 'Alamofire', :git => 'https://github.com/Alamofire/Alamofire.git', :commit => '0f506b1c45' pod 'Alamofire', :git => 'https://github.com/Alamofire/Alamofire.git', :tag => '3.1.1' pod 'Alamofire', :git => 'https://github.com/Alamofire/Alamofire.git', :branch => 'dev'
If you need to add a new dependency to your project, you simply add the dependency to your Podfile and then run
pod install. This will not reinstall your dependencies, it will instead compare the Podfile with the Podfile.lock to identify the new dependencies – and then install the new ones. All the other libraries stay the same.
Now, what happens if you want to update ALL your dependencies. Running
pod install is not going to work because it will just install the new dependencies. Instead, you need to use
pod update (similar to
carthage update) This command will update all the pods that are listed in the podfile. If you want to just update a single pod you can add the name to the command and only this library is going to be updated:
pod update name_of_pod (the same as
carthage update name_of_pod).
Carthage for CocoaPods Users
Right from the beginning, Carthage is very different from Cocoa Pods. It is written in Swift, not Ruby. And, crucially, it is decentralized. So there is no place where you can go and search for libraries like you can do with CocoaPods. To find your dependencies you will have to rely on Github or Google. The advantage of this approach is that there is no single point of failure: there is no translation between “name of library” and “place to download library” as happens in CocoaPods.
Carthage will only download and build your libraries (using xcodebuild). You will have to manually install each of those libraries into your project. This is a huge negative when compared to Cocoapods, where you just run a command, open the generated xcworkspace and you are good to go.
The most common command in Carthage is
carthage update. This will check out your dependencies into the Carthage/Checkouts folder and build the dependencies. If you are new to Carthage and are set at your desk wondering why
carthage update is taking soooo much longer than
pod install normally does: it’s because the first command downloads and builds everything while the second command just downloads the dependencies.
In Carthage, instead of a Podfile we have a Cartfile and a Cartfile.private. The Cartfile lists all the dependencies that you want to use in your app. The way of specifying the dependencies is similar to CocoaPods, with each dependency declared as an origin (git, github or binary) and a version requirement.
The version requirements work in a similar fashion to CocoaPods.
>= 1.0At least version 1.0~> 1.0Compatible with version 1.0== 1.0Exactly Version 1.0″commit-branch-tag”Specified git object
This is what a Cartfile looks like:
github "realm/realm-cocoa" "v3.3.0" github "Alamofire/Alamofire" ~> 4.7 github "airbnb/lottie-ios" "2.1.4"
As you know, CocoaPods creates a .xcworkspace with your libraries integrated into it. Carthage is different, after running
carthage update, you will see the built libraries, but it’s up to you to grab those and integrate them into your project. The compiled libraries are going to show up in the
carthage/build/ folder. If you want to follow a detailed tutorial on how to integrate these libraries there is no better place than the official documentation: Adding frameworks to an application.
carthage update finishes running, you will find a new file in your directory called Cartfile.resolved. This file has the same function as Podfile.lock and lists which versions were built for each dependency. This is really useful for when you want to build the same dependencies that were generated in the initial
So far, we have only discussed
carthage update. If we want to run a project that is already using Carthage we can’t call this command, we need a command that grabs our Cartfile.resolved checkouts and builds the specified dependencies and versions listed there. In this scenario, we need to use
carthage update is going to fetch the latest versions specified in our Cartfile and
carthage bootstrap is going to use the Cartfile.resolved to resolve the dependencies.
But what should we do if we want to add a new dependency? Well, we can’t run
carthage update because that may update some versions of the dependencies. Nor can we run
carthage bootstrap. That dependency has never been resolved before, so it’s not in the Cartfile.resolved. In this situation, we need to run
carthage update name_of_dependency.
Let’s look at an example: if we have the following Cartfile and we just want to update (or install for the first time) a library:
github "realm/realm-cocoa" "v3.3.0" github "Alamofire/Alamofire" ~> 4.7 github "airbnb/lottie-ios" "2.1.4"
We will run:
carthage update Alamofire lottie-ios
Finally, let’s look at speeding things up a little! When you run
carthage update you’ll notice that it can take fooooooorever to finish. This is because Carthage will clone the library, build it and leave it ready to use as a framework. But we can speed this process up significantly!
I created a new project and added the following to my Cartfile:
github "Alamofire/Alamofire" ~> 4.7.2
And now we run the update:
$ carthage update *** Fetching Alamofire *** Checking out Alamofire at "4.7.3" *** xcodebuild output can be found in /var/folders/zv/10lxs76s7sq28fjhjfslcj_00000gn/T/carthage-xcodebuild.Gxkmfn.log *** Building scheme "Alamofire macOS" in Alamofire.xcworkspace *** Building scheme "Alamofire watchOS" in Alamofire.xcworkspace *** Building scheme "Alamofire iOS" in Alamofire.xcworkspace *** Building scheme "Alamofire tvOS" in Alamofire.xcworkspace
As you can see here Carthage is building three different schemas (one per platform) but as my app is only for iOS, I’m losing a ton of time. The first optimization is to tell Carthage to just update for the platforms I’m using:
$ carthage update --platform iOS *** Fetching Alamofire *** Checking out Alamofire at "4.7.3" *** xcodebuild output can be found in /var/folders/zv/10lxs76s7sq28fjhjfslcj_00000gn/T/carthage-xcodebuild.eWEzEL.log *** Building scheme "Alamofire iOS" in Alamofire.xcworkspace
Now that is faster, but Carthage is still fetching and rebuilding a framework that already exists.
$ carthage update --platform iOS --cache-builds *** Fetching Alamofire *** Checking out Alamofire at "4.7.3" *** xcodebuild output can be found in /var/folders/zv/10lxs76s7sq28fjhjfslcj_00000gn/T/carthage-xcodebuild.OZTapd.log *** Valid cache found for Alamofire, skipping build
--cache-builds tells Carthage to avoid rebuilding a dependency if it’s already built. And of course we can use the same flags for a
carthage bootstrap and we would end up with:
$ carthage bootstrap --platform iOS --cache-builds *** Checking out Alamofire at "4.7.3" *** xcodebuild output can be found in /var/folders/zv/10lxs76s7sq28fjhjfslcj_00000gn/T/carthage-xcodebuild.a134Yj.log *** Valid cache found for Alamofire, skipping build
This is telling Carthage to “Download and build the versions of the libraries already resolved (Cartfile.resolved) and only build for iOS those that have never been built before”.
Swift Package Manager
Swift Package Manager (SPM) was introduced with the release of Swift 3.0. The good news is that it is shipped with Swift, so it comes pre-installed. It’s designed to automate the process of downloading, compiling and linking the dependencies. An interesting feature I noticed is that SPM runs on Linux as well as macOS.
It’s decentralized (like Carthage) and open source (like CocoaPods and Carthage).
SPM is new and as a result, it’s still pretty limited. Right now it doesn’t support iOS, watchOS or tvOS platforms. There are some workarounds to make SPM work on these platforms, but they feel ‘dirty’ and are not supported by default. I wouldn’t encourage you to use them. Another drawback is that SPM only supports Swift. If you want to use any of the awesome libraries out there that are written in Objective C – tough – you won’t be able to do it with SPM.
So, if it can’t be used on iOS, watchOS or tvOS…why bother? Well, you can use it for any type of application that doesn’t run on those platforms. For example, Perfect, Vapor and Kitura, which are Swift server-side frameworks, use SPM.
The limits of SPM make it hard to compare to CocoaPods or Carthage. I suggest you sit out this round and wait to see what Apple brings in the new iterations of SPM!
Would you like to work with Andres? Get in touch with us and we’ll help you hire a great developer for your next project.