Sunday, October 27, 2019

Dependency Heck

The state of iOS dependency management in 2019 is not great, and might get worse before it gets better. 


A Brief History

For years we had CocoaPods. Love it or hate it, it was all we had [1]. 2014 brought us Swift and dynamic framework support in iOS 8. Shortly thereafter Carthage was born. It was written in Swift and took advantage of the new dynamic framework support. Carthage has a different philosophy from CocoaPods. CocoaPods modifies the project file to pull the sources from the various dependencies into the build. Carthage prefers more of a hands-off approach which requires a little more from the application developer in exchange for more control and flexibility. Furthermore Carthage separates the building of the dependencies from the building of the consuming project, which allows the option of using pre-built binary frameworks [2]. Also, CocoaPods requires library vendors to provide a “pod spec” to define the contents of the library and the rules to build it. This often means maintaining two sources of truth because an Xcode project file is still desirable for framework development. Carthage uses just the standard Xcode project file. Today most framework projects support both Carthage and CocoaPods, to the credit of the framework authors. There are valid reasons why application developers prefer one over the other. 

Enter the Swift Package Manager

In 2015, many people rejoiced to learn that Apple was working on the Swift Package Manager. It sounds great right? A dependency manager built and supported by Apple themselves. Well, we had to wait nearly four years for SwiftPM support to be added to Xcode, and for it to be usable with iOS projects (instead of just macOS and Linux projects).

But now things are good, right? Not exactly. SwiftPM has a few crucial limitations. 
  • It does not support resources. If your framework includes icons, Xibs/Storyboards, or anything else that isn’t source code, you’re out of luck [3].
  • It does not support mixed source frameworks. While SwiftPM supports several different languages, you can only use one language per package target. For example, you can’t have a target with both Swift and Objective-C code. 
  • There is no support for binary-only dependencies. If you need a binary framework such as Twilio or Mapbox, you’re out of luck. This can be particularly bad if you have modularized your development. For example, perhaps your app has a map feature that was developed as a framework, and the feature uses MapBox. The app doesn’t directly use MapBox but the feature framework declares the dependency on MapBox. 

Will it Work?

At my place of work we have multiple mobile teams and multiple apps. We use a handful of 3rd party frameworks, and many internal frameworks of our own making. We’ve used CocoaPods in the past, but switched to Carthage a couple of years ago. Carthage has problems [4], but it is still probably the better solution for our use case at this time. I wondered how far we could get if we tried to switch to SwiftPM. Some of the features of SwiftPM (such as the ease of switching frameworks in and out of “development mode”), as well as the Xcode integration are very compelling. I knew about some of the aforementioned limitations, but suspected we might be able to workaround some of them. For example, I’m pretty sure we could workaround the resource limitation with some custom tooling and conventions. I started with the leaf nodes of our dependency graph and worked up. Adding `Package.swift` files to the projects was easy enough, and felt good. Before long I ran into the mixed source problem. We have a framework that contains Swift and Objective C++, so that was a no-go. I thought we might be able to split it into multiple frameworks with each one only having source files of only one type. I wasn’t able to get that to work either. This framework worked fine with both CocoaPods and Carthage. 

So What Now? 

We don’t know how long it will take for SwiftPM to be feature-rich enough for most people to use. I don’t even know if that is their goal. My fear though is that the existence of SwiftPM, especially now that it is integrated into Xcode and declared to be some form of “done,” will kill off development of CocoaPods and Carthage [5]. Also, framework vendors, tired of supporting multiple dependency managers, might adopt SwiftPM and drop support for Carthage and CocoaPods. 

What Can You Do? 

If you have the patience for it, you could contribute to SwiftPM. The non-Xcode parts of it are open source, after all. If you use Carthage or CocoaPods, consider contributing to those projects as well. They could use your help right now.  If you are a framework author, please continue to support Carthage and CocoaPods for your framework. Maybe your framework is pure Swift with no resources, so SwiftPM works fine for you. However, many people who would like to use your framework may have other dependencies that can’t support SwiftPM any time soon. The framework vendors that support all of the dependency managers are the heroes we need but don't deserve [6]. You could also file radars (or “feedbacks”) about the limitations of SwiftPM, if you’re into that sort of thing. 



[1] Yes, I know there are other options, such as pulling 3rd party code into your project and managing versions yourself, or refusing to use 3rd party code. 

[2] There are a lot of limitations with pre-built binaries due to the ever-changing Swift ABI. Even the stable ABI introduced with Swift 5.1 comes with a lot of tradeoffs and caveats. It also requires framework vendors to opt-in. See WWDC2019 session 416. 

[3] There is an active proposal to add support for resources. Hopefully it won’t take years to complete. https://forums.swift.org/t/draft-proposal-package-resources/29941

[4] The dependency resolver is quite buggy for moderately complex dependency graphs. There seem to be concurrency bugs related to fetching git repos. Builds can be slow because they are not parallelized, it builds for all architectures though you might not need them, and it builds all frameworks in a repo even though you might only use one (for repos containing multiple frameworks).