Managing Internal iOS Dependencies with Private CocoaPods
Strava’s iOS application and extensions are powered by a lot of code. As with most apps, this is a combination of code written by Strava engineers and by various third parties. We’ve been managing these third party dependencies using CocoaPods, a dependency manager for Swift and Objective-c Cocoa projects, for a few years.
CocoaPods allowed us to easily share third party libraries such as AFNetworking
or Typhoon
between different targets inside the iOS application. For example, both our iOS application and our Today extension, which are separate binary targets, need access to AFNetworking
for network requests. This is as easy as adding pod AFNetworking
to each target's portion of our Podfile
, and voila!
We liked CocoaPod’s portability and ease of use for managing third party dependencies so much that we wanted to enable our engineers to do this for our internal app code as well. Luckily, CocoaPods provides a guide for doing just that. Strava’s iOS application has a handful of localized formatters and model types, for example, that need to be shared between different targets. Sharing this code allows our iOS and watchOS applications to represent data, visually and programmatically, in the same way with ease. This required us to refactor this shared internal code dependency into a separate framework inside the project.
Before using CocoaPods to manage these internal target dependencies, we would have to manually link the framework to various targets inside of our Xcode project. This manual linking added bloat and meant that any changes to the framework were immediately included in all platform targets upon commit. We also wanted to write a mobile client segment matching library, which meant that we were writing Android and iOS frameworks that would ideally share the same test data for verification.
Benefits
By switching to CocoaPod’s for our internal dependencies we gained the following benefits:
Separate Git repository on a per framework basis.
- The nature of private CocoaPods means that the framework code exists in a separate repository.
- Separate repositories make it less likely to have merge conflicts in framework code.
Decreased project/code conflicts.
- Dependencies are managed explicitly inside the
Podfile
instead of in our Xcode project file. Fixing merge conflicts means fixing thePodfile
and runningpod install
to have CocoaPods make it right.
Framework code can live amongst other platform code/data.
- Related Android/iOS libraries can live in the same, logical repository with the same verification data.
Easy framework versioning using Strava’s internal CocoaPod source repository.
Increased portability because of defined CocoaPod structure.
- Smart organization allowed us to include a pod’s subspec in our Apple Watch extension with little change even though the full pod was only iOS compatible.
Seamless multi-target framework linking handled by CocoaPods.
- Add
pod StravaKit
to the target in thePodfile
and you're 👌🏽
Fast main app-independent development, compile and test time.
- Our main app target and tests contain 1000+ source files while our frameworks typically contain under 50 source files. Decoupling the time to test changes in these code bases from the main app enables faster development. Rather than taking 30 minutes of CI compute time it takes just seconds, making the test time more proportional to the size of the changes.
Our CocoaPod structure
This section will detail the changes and repositories involved in using and modifying our internal CocoaPods. The Podfile
changes below allow CocoaPods to find our internal pods. The private framework repositories and the private spec repository allow engineers to create and contribute to our internal pods.
Podfile
According to CocoaPods, the Podfile
is "a specification that describes the dependencies of the targets of one or more Xcode projects." We had to make changes to our Podfile
to use private and public CocoaPods in tandem. To allow the CocoaPods app to locate our internal pods in our private spec repository we added a new source directive:
source 'https://github.com/CocoaPods/Specs.git'
source 'https://github.com/strava/specs.git'
These source directives live at the top of our Podfile
. The first source is the CocoaPods main trunk where all public pods store their podspec
s and the second is the repository where Strava engineers push updates to our internal pods. This addition allows engineers to use our internal pods as though they were regular public pods, and without declaring a separate source on a per pod basis.
Private Framework Repository
All individual pods are stored in a separate repository. These frameworks have their own tests and, potentially, example applications to demonstrate functionality. CocoaPods allows us to declare inter-pod dependencies with ease in the individual Git repositories. For example, our iconography pod, that contains our programmatically drawn icons, can depend on our model pod, that contains our model objects. CocoaPods does all of the heavy lifting by sorting out these dependencies when the pods are incorporated into our main application.
The workflow for contributing to these frameworks is as follows.
Development
The engineer altering the pod framework makes code changes in the separate repository. It is useful for engineers to use their framework changes alongside the main application, at times, for validation and testing. This can be done by the contributing engineer or by the reviewer. CocoaPods makes this easy with the :path
Podfile
directive.
- Clone the framework repository to the local system.
git clone https://github.com/strava/STRVIcons.git ~/Developer/Strava/icons
- Checkout the branch in the framework that needs reviewing.
cd ~/Developer/Strava/icons
git checkout origin/awesome.framework.change
- Point the
pod
path in the main app'sPodfile
to the location of the cloned Git repository on their system.
pod 'STRVIcons', :path => '~/Developer/Strava/icons'
- Run
pod install
in the main app directory to sync thePods/
workspace with the newPodfile
. This symbolically links the pod's source files into our main app workspace, which allows changes to the pod to be made directly in that workspace. - Build and run main app like normal.
Submitting Changes
- Push code to these repositories using a normal GitHub Pull Request flow (open Pull Request, solicit code review, CI checks, etc.).
- Validate the current, local state of the pod using
pod lib lint
and a reference to any source dependencies.
pod lib lint STRVIcons.podspec --private --sources=https://github.com/strava/specs.git,master
--sources
keyword is required when the pod has dependencies on another internal CocoaPod.--private
allows us to avoid warnings that don't matter for private pods like a non-reachablehomepage
value.- Version the pod by altering the
version
value in thepodspec
...
~/D/S/icons> git diff
diff --git a/STRVIcons.podspec b/STRVIcons.podspec
index 51f59f9..1d8e4bd 100644
--- a/STRVIcons.podspec
+++ b/STRVIcons.podspec
@@ -8,7 +8,7 @@Pod::Spec.new do |s|
s.name = 'STRVIcons'
- s.version = '0.2.0'
+ s.version = '1.0.0'
- …and push a Git tag that matches the
version
.
git tag 1.0.0
git push origin 1.0.0
- The new
podspec
must be pushed to the private spec repository. Thepod repo push
command is detailed in the next section. We have created aMakefile
in many of our pods to ease this process.
Private Spec Repository
As mentioned above, Strava has an internal CocoaPod spec repository where we store our pod versions. This repository contains all podspec
files with an associated version and a similar public example is here. These podspec
files have information about a given pod and point CocoaPods to a specific tag in one of our framework repositories. The structure of this spec repository is maintained by the pod repo push
command in CocoaPods. We don't manipulate it manually.
The following will detail what’s required to push new versions of our pods. These actions are only required when mutating internal pods since the source
directive in the Podfile
allows CocoaPods to interface with our internal spec repository when running pod install
(to sync the project with the Podfile.lock
/Podfile
file) or pod update
(to update a given pod included in our Podfile
).
- The reference to this repository must be added to the CocoaPods Ruby gem on your system.
strava
is typically the name we give the local copy of our internal spec repository.
pod repo add strava https://github.com/strava/specs.git
- The local copy of the spec repository can become out of date. We run this to sync it with the remote repository.
pod repo update strava
- The new version of the internal CocoaPod must be pushed so we can include it in our main app code. We run this to push the latest framework repository’s
podspec
to our internal spec repository.
pod repo push strava ~/Developer/Strava/icons/STRVIcons.podspec
Caveats
- We currently version and update each pod manually. We are looking to automate this in the future but our pods tend to rarely change.
- We don’t manually diff
Pods/
changes in our main app repository. We re-run the desired pod command (pod install
orpod update
) after sorting outPodfile
andPodfile.lock
collisions. - Keeping everyone on the same CocoaPods gem helps avoid collisions.
- We check in our
Pods/
folder to our main application repository. This allows initial setup and build without any CocoaPods knowledge.
Difficulties (it’s not all 🌈 and 🦄)
The following are some difficulties we’ve experienced with our private CocoaPod structure.
Internal CocoaPod name that matches a public CocoaPod.
- See StackOverflow here.
- CocoaPods defers to the
master
trunk when doingpod
operations. This results in our internal CocoaPod getting skipped for the public pod with the same name. - We ended up changing our internal pod name to easily get around this issue. And we won’t tell you what it is. 😀
Sparse information on maintaining CocoaPods.
- We started using this structure more than 2 years ago and the documentation/CocoaPods community has come a long way.
Localized string/resource bundling, in general.
- We manually include a string bundle from our localized formatter pod in our main app, localize it separately from the pod, and ensure the pod files load the bundle by name.
External Pull Request review and multiple levels of review take time.
- The external nature of the code is great for many things. It ends up being harder to solicit reviews when it’s separate from our typical day-to-day codebase.
Lack of understanding through team about how CocoaPods work.
- Like any new technology, this requires an engineer to learn how to use it and creates a natural barrier to entry, despite it making things easier once he/she is comfortable.
Finally
In closing, using CocoaPods to manage both internal and external dependencies has made development easier and faster over the past two years. Our infrastructure for dealing with the various repositories continues to evolve and become more automated over time.
Also, Strava is hiring! If you’re interested in working on Strava’s engineering team, you can also reach out to me directly @m4ttrob.