Part 2 in a series of tutorials on modern app infrastructure:
CocoaPods is the world’s largest collection of pre-built, open source modules for iOS developers – some 46,000 at the time of writing.
Although it’s far from perfect (I have complained about it a great deal in the past), CocoaPods has done more to streamline developer productivity than any other piece of our infrastructure: you can literally pull incredible functionality into your app in just a few seconds, and keep it updated over time.
This is part two of a tutorial series about upgrading your apps to use modern infrastructure – tools that help you write better software by automating tasks. In this installment we’re going to look at another problem our test app has: although it uses SwiftyBeaver for logging, it does so by literally copying the files directly into the project. While this certainly works it means any future updates to SwiftyBeaver won’t automatically be merged into our app.
CocoaPods is designed to solve this. Rather than copying someone else’s code into your project, you reference their Git repository and a version number and let CocoaPods install it for you. Even better, you can update all your third-party modules to the latest version using a single command, bringing with them any new features and security updates.
So, we’re going to install CocoaPods then update our app so that it pulls in SwiftyBeaver using CocoaPods rather than importing the files directly.
Before we proceed, please download the test app that we’re using for this tutorial series. If you completed the first part of this tutorial series – how to refactor your app to add unit tests – you should use the code you had at the end.
Warning: The example project has been written specifically for this tutorial series, and contains mistakes and problems that we’ll be examining over this tutorial series. If you’re looking for example code to learn from, this is the wrong place.
If you don’t already have the CocoaPods tools, the first thing you need to do is install them. CocoaPods is actually written in Ruby, but your Mac already includes the tools needed to install and run it.
So, open the Terminal app and run this command now:
sudo gem install cocoapods
That should only take a few seconds as your Mac downloads and installs CocoaPods and its dependencies.
The next step is to change into the directory where your project is. Xcode’s naming is a little unhelpful here because there’s a “Paraphrase” directory that itself contains a “Paraphrase” subdirectory, however you’re looking for the parent directory – the one where you can see Paraphrase.xcodeproj.
For me, my project directory is on my desktop, so I ran this command
If you stored it elsewhere, please adjust that command as needed.
Now for the important part: we need to ask CocoaPods to configure our project for pod usage. Please run this command:
That asks CocoaPods to create a file called Podfile in our project’s directory so that it can store our project’s CocoaPods configuration. It will look something like this:
# Uncomment the next line to define a global platform for your project # platform :ios, '9.0' target 'Paraphrase' do # Comment the next line if you're not using Swift and don't want to use dynamic frameworks use_frameworks! # Pods for Paraphrase end
# Pods for Paraphrase line is one of two we care about: we request specific frameworks there and have them installed into our project.
As I said earlier, right now the Paraphrase project works by copying all of SwiftyBeaver’s Swift source code directly into the project. We’re going to replace that with a CocoaPods-managed version instead, so under
# Pods for Paraphrase I’d like you to add this line:
That asks CocoaPods to add SwiftyBeaver to our project. It doesn’t specify any version of SwiftyBeaver, so CocoaPods will automatically use the latest version – we’ll look at alternatives to that later.
Before we move on, we need to make one more change. Near the top of your Podfile you’ll see this comment:
# platform :ios, '9.0'
We’re going to specify a deployment target of iOS 11.0, which should be fine for most apps. So, replace that line with this:
platform :ios, '11.0'
Save your Podfile then run this command from the terminal:
If you’ve used CocoaPods before, that command will take a few seconds to run as CocoaPods fetches SwiftyBeaver and prepares our project.
If you haven’t used CocoaPods before, you’ll run into arguably one of the biggest problems of CocoaPods: it uses a centralized “master spec” repository that contains metadata for all versions of all CocoaPods in their system, and that repository must be downloaded onto your computer in order to continue. Yes, metadata for all 40,000. Yes, with their version histories too.
As a result, the CocoaPods master repo will take up about 600MB on your computer at the time of writing. This will will be downloaded the first time you run
pod install on any project, then kept up to date over time. There is no way around this, I’m afraid, and if you’re on a slower or capped internet connection I think you’ll appreciate why this has been such a pain point for users in the past.
Anyway, even if you have a relatively fast internet connection this process will take a while to complete – not only must the repository be downloaded, but it must also have its information fully resolved by Git. So, go and make some coffee or tea, because it will take a while no matter what setup you have…
Welcome back! Running
pod init for the first time is always a bit of a shock to the system at first, but once it finishes you’re all set - everything is smooth sailing from here.
CocoaPods should have printed an important warning while it was working, but if you missed it I’ll repeat it here: [!] Please close any current Xcode sessions and use Paraphrase.xcworkspace` for this project from now on.
This matters. You see, CocoaPods hasn’t changed your original Paraphrase.xcodeproj file – that’s the same Xcode project it always was. Instead, it generated an Xcode workspace that wraps around your project, adding in the pods you requested. This will be called Paraphrase.xcworkspace.
Now, the reason this matters is two-fold:
So: if you had the Xcode project open, please close it.
Now that’s done, go ahead and open Paraphrase.xcworkspace. You should see both a “Paraphrase” project and a “Pods” project, and inside Paraphrase is the same code we had before – you can effectively go ahead and carry on working as you did previously, leaving CocoaPods to do its thing.
In this case we need to upgrade our project so that it uses the CocoaPods-supplied version of SwiftyBeaver rather than the one that was added by hand. This takes two steps: deleting all the SwiftyBeaver code, then importing the SwiftyBeaver module everywhere it’s needed.
So, go ahead and select the SwiftyBeaver group inside the Paraphrase project, then hit backspace to delete it. You should choose Move To Trash because all that code just isn’t needed any more.
Now when you press Cmd+B to build the project you’ll get lots of errors because Swift no longer knows what SwiftyBeaver is. This is because CocoaPods provides it was an external module that must be imported, but that’s easy to do.
In fact, all you need to do is add
import SwiftyBeaver to the top of the following files:
And that’s it – our code will compile again. Awesome!
Although upgrading our project to use CocoaPods was straightforward enough, there are three more things you’re likely to need to know going forward.
First, if you change your Podfile to add, remove, or update any pods, you should re-run the
pod install command. This will download any pods that aren’t already available and update your workspace ready to use them. On the other hand, if you want CocoaPods to update your pods to their latest versions you should run
pod update will also update the CocoaPods master spec repository, so it might take 30 seconds or so.
Second, when we added
pod 'SwiftyBeaver' to our Podfile it meant we wanted the latest version of SwiftyBeaver. This is fine while you’re actively developing something, but as you come closer to shipping you’ll want to switch to a specific version to avoid breakages happening.
CocoaPods lets you request specific versions of pods if you want – “give me 1.0 and nothing else” – but more often you’ll want a range of versions based on semantic versioning. Using semver version numbers are written as 1.0.3, where the “1” is a major number that reflects breaking changes made to code, the “0” is a minor number that reflects non-breaking additions to the code, and the “3” is a patch number that reflects bug fixes but no new features or breaking changes.
In CocoaPods you can request a range of pod versions based on your needs. Probably the most common looks like this:
pod 'Alamofire', '~> 4.7'
That means “install the Alamofire networking library, making sure I have at least v4.7 but I’m happy with anything before v5.0” Remember, when the major version changes it means breaking changes might affect your app, so restricting usage in this way is sensible.
Finally, there is some disagreement as to whether the code for your pods should be included in your source control system or not. When we ran
pod install CocoaPods created a Pods directory containing the code for SwiftyBeaver. If you delete that folder and run
pod install again, you’ll get the Pods directory back, so why include it in your source control?
Like many things in programming there are good reasons for and against. Checking pods into source control means your project can be built immediately after check out without needing any extra commands, you’re isolated from problems with GitHub (they do happen!), and you’re guaranteed to have a known-good state in your central repository.
On the flip side, not checking pods into source control will mean your repository is smaller and you eliminate the possibility of problems when merging branches that have different Podfiles.
I ran a brief Twitter poll and almost 2/3rds of folks thought checking their pods into source control was a bad idea, however I think the pragmatic thing is, as usual, to do whatever works best for your team.
Checking your Pods directory into source control is…— Paul Hudson (@twostraws) May 7, 2018
This was the second part of a short series on upgrading apps to take advantage of modern infrastructure. You’ve seen how easy it is to stop importing source code by hand and rely on a dependency manager instead, but in the following articles we’ll look at other ways computer automation can help us write better code – stay tuned!
Paul Hudson is the creator of Hacking with Swift, the most comprehensive series of Swift books in the world. He's also the editor of Swift Developer News, the maintainer of the Swift Knowledge Base, and Mario Kart world champion. OK, so that last part isn't true. If you're curious you can learn more here.