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.