Xavier Lowmiller's Blog

software developer • vegetarian • pub quiz organizer • star wars • bob dylan • member of gryffindor

What Adding Dependencies Will Do To Your App in 2020

With Xcode 11’s support for Swift Package Manager and the coming changes in Swift 5.3, there’s a lot of discussion on what dependency manager to use. I’d like to take you on a tour to see what happens to an app when dependencies are managed using the various tools.

To do this, I measured various metrics around dependencies to see what adding dependencies looks like in the various options in 2020.

Dependency Management has come a long way on iOS. In the early days, it was expected to manually add libraries and frameworks, and update them by hand. Git Submodules can be of some help here, but integrating them into Xcode projects is still tedious.

The managers

CocoaPods

As the community grew and more libraries were written, discoverability was another concern. Enter CocoaPods, which fixed all these problems: There’s a central index where pods can be discovered, and the integration into projects is now a breeze. The value of CocoaPods to the iOS community cannot be described by words. It’s such a great example of what a community can do, and has been the de facto default package manager for iOS projects for years. Its impact on the whole iOS ecosystem can’t be overstated. The website is the go-to place for discovering iOS dependencies. Publishing a library or framework for iOS without CocoaPods support is barely imaginable.

This is the Podfile used for the experiments can be found here.

Carthage

Shortly after Swift came around, Carthage was born. It’s written in Swift itself, and has some different opinions than CocoaPods. It focuses on simplicity and ease of understanding how it works. This means there’s less magic, both in the good and in the bad sense. It’s the simple (not easy) dependency manager for Swift projects. There’s some beautiful ideas behind Carthage, like being decentralized and imposing as few changes to your Xcode project as possible. There’s a great writeup on their GitHub page that I don’t need to repeat here. The bottom line is that it’s simpler than CocoaPods but doesn’t have (and won’t ever have) as many features. This makes the setup a little more work, but once it’s done it’s unlikely to need adjustments in the future.

This is the Cartfile used for the experiments can be found here.

Swift Package Manager

The new kid on the block for dependency management on iOS. At WWDC 2019, Apple announced that the Swift Package Manager (which has been around since late 2015) would be fully integrated in Xcode and could manage iOS projects. This is big news for a platform that never had a truly official package manager! A lot of popular libraries have since adopted SPM and it’s only going to grow bigger as important features land in SPM in Swift 5.3, such as support for assets.

Alternatives Considered

In this post, I drilled down the most popular ways to manage dependencies using their most basic configurations. But of course, there’s many more ways to manage dependencies.

Manual Dependency Management

A.k.a. just adding files to your project. It would have been nice to do this so I could have a baseline without any kind of module/framework/Swift Package for the measurements. This probably works nicely with single-file dependencies (RNCryptor even recommends this technique), but managing more complex dependencies is tedious, especially since there’s not way to easily update things. I tried this, but it’s just not feasible with so many dependencies, I had too many name clashes and other issues, like libraries expecting to be embedded in modules.

Git Submodules

The dependency manager when there is no dependency manager. The idea here is to add projects as git submodules and integrate their .xcodeproj files into your project’s project file or workspace. This is a lot of setup work, and should have similar results as CocoaPods.

CocoaPods with static libraries

In the early days of Swift, it was only possible to package Swift Modules as dynamic frameworks. This is still the default for most CocoaPods, so I used that in my measurements.

The dependencies

To do my measurements, I chose 10 dependencies that should be a somewhat realistic mix of popular libraries. They must be compatible with all dependency managers and for simplicity’s sake should be on GitHub. Some of them have dependencies themselves. They range from small (Reachability.swift, 1 file, 275 LoC) to large (RxSwift, 141 files, 10330 LoC).

Here’s the list:

All measurements were done on my developer machine, a MacBook Pro (15-inch, 2017), using Xcode 11.5 and iOS 13.5. If you’d like to follow along, all of my experiments are on GitHub.

Round 1: App Launch Times

Historically, app launch speed has been negatively impacted, especially if you have a lot of libraries. With the arrival of dyld 3, a lot has changed here. Let’s see if launch times are still an issue for todays iOS dependency setups.

I tested this using the new default launch test on both a physical iPhone 11 Pro as well as the iOS Simulator.

This is the test case:

func testLaunchPerformance() throws {
  measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) {
    XCUIApplication().launch()
  }
}

The hypothesis is that an app with 10 dependencies launches slower than an app that has no dependencies at all. The results, however, show that there is no longer much of a measurable impact:

  No dependencies CocoaPods Carthage SPM
iPhone 11 Pro 1.009s 1.021s 1.105s 1.032s
Simulator 1.056s 1.105s 1.118s 1.080s

There’s still a little difference between the different methods, but that can very well be a measurement error. All setups that use dependencies launch a little slower than the “No dependencies” setup, but it’s not any serious amount of time. At 10 dependencies, it’s probably not worth optimizing for.

Winner: Tie!

Round 2: App Size

App download size has always been an important metric, especially before Apple effectively dropped the mobile app download size limit.

Let’s find out how integrating dependencies will affect your app’s size!

Round 2a: xcodebuild build

Running xcodebuild will create a build product in the DerivedData directory that basically is a runnable app. We use this command to generate it:

xcodebuild build \
  -quiet \
  -scheme Dependencies \
  -configuration Release \
  -derivedDataPath DerivedData

We can measure the file size of the artifact using du:

du -sh DerivedData/Build/Products/Release-iphoneos/Dependencies.app

Here are the results:

  No dependencies CocoaPods Carthage SPM
Total Size 140 KB 14 MB 84 MB 12 MB
Executable Size 104 KB 104 KB 104 KB 12 MB
Frameworks Folder Size No Frameworks 14 MB 84 MB No Frameworks

84 Megabytes! Yikes!

I measured the binary executable size separately from the Frameworks folder, which are the two places where compiled code lives. It looks like SPM wins this round, but apps are normally not distributed in a universal, uncompressed way. Let’s see what a thinned app looks like!

Round 2b: App Thinning

iOS 9 introduced App Thinning, which is a technique to reduce .ipa file sizes: By default, the App Store will generate specialized versions for each kind of device, stripping away unneeded assets and executable code. Let’s see how using App Thinning changes the numbers!

First, we build and export an archive so we can export thinned apps locally:

xcodebuild archive \
  -quiet \
  -scheme Dependencies \
  -configuration Release \
  -archivePath 'DerivedData/archive.xcarchive' \
  -derivedDataPath DerivedData

Then, we can build thinned versions for specific devices:

xcodebuild build \
  -quiet \
  -configuration Release \
  -exportArchive \
  -archivePath 'DerivedData/archive.xcarchive' \
  -exportPath 'DerivedData/thinned-ipa' \
  -exportOptionsPlist ../exportOptions.plist

The exportOptions.plist contains the device identifier of the model we’d like to thin for, in this case it’s an iPhone 11 Pro:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
        <key>thinning</key>
        <string>iPhone12,3</string>
</dict>
</plist>

This will create a thinned, zipped .ipa file that we can measure again:

  No dependencies CocoaPods Carthage SPM
Total Size 24 KB 2.1 MB 2.2 MB 1.7 MB
Total Size (unzipped) 136 KB 6.2 MB 6.5 MB 5.0 MB
Executable Size 88 KB 88 KB 88 KB 5.0 MB
Frameworks Folder Size No Frameworks 6.4 MB 6.0 MB No Frameworks

This is a pretty neat result! Carthage still produces the largest artifacts, but it’s nowhere near the difference as in the un-thinned test. SPM is definitely a good option when in comes to size, as it produces the smallest files. The ipa produced here is around 20% smaller than the others. This might be due to embedding dependencies statically, so a similar result can might be achieved using CocoaPods in the static library mode.

Winner: Swift Package Manager!

Round 3: Build Times

Having fast feedback from CI and quick builds on developer machines is always a goal worth spending time on. Let’s see how the different package managers perform!

I measured the following things:

  No dependencies CocoaPods Carthage SPM
Dependency Resolution - 1m 38s 13m 25s 2m 23s
Clean Build 3s 1m 10s 5s 1m 20s
Incremental Build 2s 3s 2s 3s
DR + Build 3s 2m 32s 12m 39s 2m 31s

The clear outlier here is Carthage. With it building all the frameworks during the dependency resolution phase (which is far slower than the others), it wins at clean builds.

During day-to-day development (where you’re mostly doing incremental builds), there’s almost no difference, at least in this rather artificial project.

Please take these results with a grain of salt. There’s no caching involved, so especially the CI use case is probably not a realistic scenario.

Winner: Hard to say, but probably Cocoapods with Swift Package Manager as a close second.

Conclusion

Things are looking pretty good for Swift Package Manager. It produces small binaries and builds as fast as Cocoapods. Once all your dependencies are available on SPM, there’s no reason not to switch to it. That said, all three dependency managers I compared are definitely viable in 2020, so there’s also no compelling reason to change dependency managers.

I think it ultimately comes down to team preference or some nuances from the random tidbits section which one they should use, with Swift Package Manager being my recommendation if you can get away with it.

Random Tidbits

Posted 04 Jun 2020