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
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.
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
Podfileinstead of in our Xcode project file. Fixing merge conflicts means fixing the
pod installto have CocoaPods make it right.
- Dependencies are managed explicitly inside the
- 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.
pod StravaKitto the target in the
Podfileand 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.
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
podspecs 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.
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
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
podpath in the main app's
Podfileto the location of the cloned Git repository on their system.
pod 'STRVIcons', :path => '~/Developer/Strava/icons'
pod installin the main app directory to sync the
Pods/workspace with the new
Podfile. 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.
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 lintand a reference to any source dependencies.
pod lib lint STRVIcons.podspec --private --sources=https://github.com/strava/specs.git,master
--sourceskeyword is required when the pod has dependencies on another internal CocoaPod.
--privateallows us to avoid warnings that don't matter for private pods like a non-reachable
Version the pod by altering the
versionvalue in the
~/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
git tag 1.0.0 git push origin 1.0.0
podspecmust be pushed to the private spec repository. The
pod repo pushcommand is detailed in the next section. We have created a
Makefilein 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 file) or
pod update (to update a given pod included in our
The reference to this repository must be added to the CocoaPods Ruby gem on your system.
stravais 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
podspecto our internal spec repository.
pod repo push strava ~/Developer/Strava/icons/STRVIcons.podspec
- 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 update) after sorting out
- 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
mastertrunk when doing
podoperations. 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. 😀
- CocoaPods defers to the
- 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.
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.