A Crash Course in iOS Dependency Management

How to use RVM, bundler, and CocoaPods to make reliable builds.

Posted on January 21, 2017 by Mike Buss

When building software it's important to lock down any dependencies you use. Not only will this ensure you can reliably build older versions of your app, it can also help your app architecture. If you can refactor your code into individual components (in this case, pods), and reliably integrate them back into your app, you can clean your main code base and increase reusability.

In this article, I'll cover how to manage iOS dependencies in a solid, reliable way with using CocoaPods, Bundler, and RVM.


Definitions

What is CocoaPods?

CocoaPods is a package manager for Xcode projects that lets iOS and macOS developers share code. Libraries distributed through CocoaPods are called pods.

What is Bundler?

Bundler is a package manager for Ruby. Since CocoaPods is implemented as a Ruby gem (a standard format for distributing Ruby programs and libraries), we can use Bundler to install and lock down its version.

What is RVM?

Ruby Version Manager (RVM) is a command-line tool for managing multiple Ruby environments.

In order to run Bundler, your system must first have a version of Ruby installed. Ruby 2.0.0 comes pre-installed on macOS Sierra, but many people recommend against installing gems to the system (using sudo gem install bundler, for example) because it has a global effect on all users.

RVM solves this problem by allowing for multiple Ruby environments on your system. After installing RVM, switching Ruby and gem versions is as simple as changing directories in the command line.


Getting Started

Locking the Ruby Version

To begin, let's lock down the version of Ruby we'll use inside our project for running tools like Bundler and CocoaPods.

First, install RVM. Then, create a file called .ruby-version in the root directory of your project. This file will specify what version of Ruby your project relies on. The contents of your .ruby-version should specify the Ruby version on the first and only line of the file, like so:

2.3.1

Now, whenever you cd into your project directory, RVM will switch your environment to Ruby 2.3.1.

To test this, try running cd .. and then cd [MyProject]. When you run which ruby, you should see something like:

/Users/mbuss/.rvm/rubies/ruby-2.3.1/bin/ruby

Now that your project is locked to Ruby version 2.3.1, you can install Bundler with:

$ gem install bundler

This will install bundler into the ruby-2.3.1 folder inside RVM, and will ensure Ruby scripts are run with Ruby 2.3.1.

$ which bundler
/Users/mbuss/.rvm/gems/ruby-2.3.1/bin/bundler

Locking the CocoaPods Gem Version

We just finished locking down the version of Ruby to 2.3.1, which will be used to run Bundler. Now, how do we lock down the version of both Bundler and CocoaPods?

Bundler uses a file called a Gemfile for specifying dependencies. If your project doesn't already have a Gemfile, you can create a sample one by running:

$ bundle init

This will create a file called Gemfile in the current directory. To have Bundler install CocoaPods, change your Gemfile to have these contents:

source "https://rubygems.org"

gem "cocoapods"

To install CocoaPods, run:

$ bundle install

The output will show you what version of CocoaPods was installed. Because the version is not specified in the Gemfile (we'll do this later!), Bundler will install the latest version of CocoaPods. At the time of writing this, the latest CocoaPods is version 1.1.1.

But how did we lock the version of CocoaPods if we didn't manually specify the version in the Gemfile? In order to understand this, you need to understand lock files.

Understanding Lock Files

Lock files keep track of dependency versions after you've installed them. Because we installed CocoaPods version 1.1.1, the Gemfile.lock file will say:

GEM
  remote: https://rubygems.org/
  specs:
    ...
    cocoapods (1.1.1)
    ...

PLATFORMS
  ruby

DEPENDENCIES
  cocoapods

BUNDLED WITH
   1.14.0

This is a truncated snippet and doesn't include the other dependencies needed for the core CocoaPods gem, but notice the version is explicitly noted in this generated file. Any future runs of bundle install will install CocoaPods version 1.1.1.

Note, too, that the version of Bundler used to generate the Gemfile.lock is listed. If you attempt to run a bundle command with a different version of Bundler installed, a warning will be thrown. If you have a different version of Bundler, you can install the correct version with:

gem install bundler -v 1.13.6

Lock files like Gemfile.lock and Podfile.lock MUST be committed into source control to correctly manage your dependencies.

Now lets say a new version of CocoaPods (1.2.0) is released. What happens if you run bundle install?

Nothing! Bundler will still install version 1.1.1 because it is specified in the Gemfile.lock.

To install the latest version of CocoaPods, you will need to run a bundle update. This will go through and update every gem in your Gemfile that does NOT have a version manually specified. In this case, it would install CocoaPods version 1.2.0 and write the change to the Gemfile.lock. All subsequent bundle install commands will install CocoaPods version 1.2.0.

Now, let's say you want to update CocoaPods, but NOT update another gem in your Gemfile. You could manually specify a gem version in your Gemfile like so:

source "https://rubygems.org"

gem "cocoapods"
gem "fastlane", "2.9.0"

With this Gemfile, what will happen if you run a bundle install? In this case, Bundler will install the version of CocoaPods specified in the Gemfile.lock, and version 2.9.0 of the fastlane gem.

What about if you run bundle update? In that case, Bundler will update CocoaPods to the latest version because its version is not specified in the Gemfile, and install version 2.9.0 of the fastlane gem.

This concept of installing dependencies, locking their versions in a lock file, and upgrading dependencies without manual version declarations applies to both gems (with Bundler and Gemfile/Gemfile.lock) and pods (with CocoaPods and Podfile/Podfile.lock).

Locking Pod Versions

Locking pods can be done in the same way as with gems, but using a Podfile:

target 'MyApp'
pod 'MTBBarcodeScanner'

Or, if you would like to lock the MTBBarcodeScanner pod to version 3.1.0 so a pod update will not update it, you can use:

target 'MyApp'
pod 'MTBBarcodeScanner', '3.1.0'

These versions will be locked in the Podfile.lock in the same way as the Gemfile.lock, and will ensure your dependencies are kept under control.


Conclusion

After integrating this dependency management scheme into a few projects, you'll see the small time investment is well worth the effort.