CocoaPods (or How to Stop Worrying About Dependency Management)
-
Mason G., Software Engineer
- Apr 1, 2015
Yelp has had an iOS app for as long as third-party iOS apps have existed. Maintaining a codebase with that much history is always interesting and sometimes challenging, and one of the biggest challenges is dependency management. For a long time, git submodules met most of our needs and caused relatively few headaches. However, the submodule approach made it difficult to understand what unanticipated or even breaking changes will be introduced when bumping a submodule by a commit – or several. A Git SHA has no concept of versioning.
Additionally, adding a new library often required changes to the build settings of the Yelp app. Sometimes new header search paths were required, other times special build flags had to be added. As the project became larger and more complicated, the process of adding a new dependency became more and more difficult. It was not uncommon for the integration of a new library to require around a day of developer time. There had to be a better way.
Enter CocoaPods: a dependency manager for Objective-C projects.Ruby has gem+bundler, and Node.js has npm. Each of these tools operate in a similar way. Your application specifies the libraries and which versions to use. The concept of a dependency manager is nothing new. Python has pip and easy_install, depends on, and the dependency manager recursively resolves these dependencies until a final list is compiled. The manager then downloads the required libraries and makes them available to your project.
Since Objective-C is a compiled language, CocoaPods provides the additional convenience of configuring header search paths, setting build flags, etc., on a per-library basis. These options are specified by the author of each library as part of its podspec, neatly shifting the cognitive load off of you, the developer.
Libraries in CocoaPods – called pods – are versioned according to the Semantic Versioning standard. This scheme makes it infinitely easier to understand when changing a library version is safe and when it will introduce backwards-incompatible changes.
CocoaPods even has built-in support for all of our internal libraries – converting our existing submodules was a breeze. We first created a new Git repository, YelpSpecs, as an internal counterpart to the main CocoaPods Specs repository. Then, after writing a podspec for each internal library and pushing those specs to YelpSpecs, our internal libraries could be referenced alongside the external libraries in our Podfile. At last count, we had eleven internal libraries referenced in YelpSpecs.
Extracting our code into pods enabled us to do a lot of cleanup. Submodules no longer needed an xcodeproj file, as CocoaPods handled all of the configuration for us. In the end, we were able to delete thousands of lines from configuration files.
Having an easy dependency management system has encouraged us to better organize our code. Before CocoaPods, we only had 1 internal library which covered everything from networking and caching to utility views. Making a new library was so painful that we opted to throw everything together.
CocoaPods has completely changed that. We’ve begun to dismantle the mega-library, and have created several smaller and more focused libraries. This has made sharing code with our new Business Owner App incredibly easy. Our current set of libraries would have been nearly impossible to manage before CocoaPods. For example, our small utility library, YLUtils, is included in every single one of our internal libraries. Getting Xcode to link this properly would have been a nightmare, but CocoaPods has made it a breeze.
We’ve been using CocoaPods for nearly a year, and the change has allowed for some great improvements in how we work and develop. However, the transition wasn’t always easy – we had been using submodules for so long that we weren’t used to the CocoaPods workflow, where you have to checkout a local version of the pod before making changes. If you don’t checkout a local version first, changes won’t be picked up by git and might be lost the next time you run ‘pod install.’
In order to help our developers, we wrote a small script that goes in the podfile. When running pod install, it removes the write permission from all pod-related files that aren’t installed via a local pod. Xcode will then give you a helpful message, noting that the file is locked. This small change really helped with the transition, preventing future frustration.
After we had happily been using CocoaPods for several months, a new version was released. However, this new release was not backwards-compatible – if developers installed the new release on their system, they would be unable to build older versions of the app or branches that hadn’t recently been updated with master.
In order to solve this problem, we realized that we would need a dependency manager to manage our dependency manager. Since CocoaPods is itself a Ruby gem, the obvious choice was bundler. We added a shim that installs the correct version of CocoaPods locally via bundler. With this setup, developers always have access to the correct version of CocoaPods regardless of what branch or version they’re working on. If you have a large team or frequently switch between newer and older branches, we strongly recommend a configuration like this. We’ve set up a sample project to demonstrate our system – check out the readme for more details.
The work we’ve put into tuning our CocoaPods setup has already paid off. It’s made it extremely simple to create and try new libraries, which has in turn helped us make our code base more modular. Switching to CocoaPods has allowed us to spend less time fighting configurations, and more time developing great new features.
Update: Samuel Giddins tipped us off about CocoaPods plugins, so you can now lock your Pods without making any changes to your Podfile! Just install the cocoapods-readonly gem.