Since professionally working with iOS even part-time, I've sworn by CocoaPods for integrating third party dependencies. I knew how hard it was to integrate a third party library at the time: drag the relevant files in, ensure no unnecessary unit tests are included, add any required frameworks, etc. - CocoaPods does that all for me! I even wrote an article about how to integrate Cordova v2.x with it a few years ago: unfortunately it's been a pain with v3.0 and above, but I'm happy to say I have stopped working with Cordova & PhoneGap!
Things did change at WWDC 2014: as you all know, Apple unveiled Swift. As someone who also worked with Ruby, I was excited by this new programming language and was very keen to jump right in! This didn't come without its pain points though, as my work days became an increasing barrage of this dreaded message popping up on Xcode every other minute:
I still persevered, and soon a number of quality third party Swift libraries started popping up. The main drawback was integrating them - up to this point, CocoaPods created a big static library containing all of the libraries it managed. This did not work with Swift - Apple required Swift code to be distributed in a dynamic framework. For iOS, dynamic frameworks were only supported in iOS 8 and above, while Swift still worked in iOS 7.
So the best solution was adding third party Swift code via submodules, while keeping my Objective-C dependencies in CocoaPods. This in turn meant I preferred sticking to Objective-C libraries, unless either the equivalent Swift version was significantly better or there was no corresponding Objective-C library. Despite not benefiting from some of Swift's strongly typed benefits and getting
AnyObjects left, right and centre, I could stick with this for the time being.
But then a new dependency manager came into town, and it had support for Swift and Objective-C frameworks. It's called Carthage.
Carthage's developers pride their product on being simple, whereas CocoaPods is deemed to be easy. I was excited when I gave Carthage a spin for the first time: this was a possible solution to the issue of integrating Swift third party libraries, plus there's some Objective-C support in there. However, there was a problem: I still needed to support iOS 7 at the time, so I had to remain with the hard solution of using submodules for Swift dependencies.
By the time I was able to do iOS 8 only projects, CocoaPods gained support for dynamic frameworks. A lot of really good iOS developers I knew decided to switch to Carthage for good. However, while I ensured SRMonthPicker and my other open source libraries were Carthage compatible, I chose to stick with the easy dependency manager I knew and loved…… for the time being!
Fast forward to last month, and I was working on an app, which was to become NetTime. This was split into a number of projects:
- The main app
- Quick specs (or unit tests)
- UI tests (and snapshotter)
- Notification centre extension
- Native watchOS 2 app
- WatchKit extension
While there was some repetition in my Podfile, I was happily developing the app with CocoaPods. However, when it came to submission to Apple, I ran into an endless loop of rejections for working binaries via its automated pre-checks. The problem boiled down to the "Embedded Content Contains Swift" setting, which CocoaPods has its own preferred values for. The only binary I managed to get past Apple's automated checks was in fact completely broken on the watch. I started to think "this is getting hard given CocoaPods is meant to be easy: why not give this Carthage lark another go?"… and so I did!
The Carthage integration process can be done in four steps, which the developers do a great job of explaining! That is few more steps than CocoaPods two step process, but still I felt far more in control of my Xcode project now, and I have a smaller dependency listing without duplicate entries!
Most of my dependencies in my project were popular with both Carthage and CocoaPods. But all of my issues with transitioning came in pairs in this case:
- Two libraries had broken Carthage compatibility in the latest release, but resorting to either the previous minor version or the latest master fixed these!
- Two binary dependencies were taken out of package management altogether: these are
Crashlytics.framework, and I didn't mind including them manually.
- Two further libraries weren't Carthage compatible: one wasn't really needed as it just dealt with a link to the Twitter app and its fallback, which I replaced with a few lines of code. The other was a markdown library that I used for displaying (CocoaPods generated) acknowledgements, for which I used a different approach altogether (and I thoroughly recommend it).
One initial observation is that despite passing in a parameter of
--platform ios,watchos (skipping unnecessary Mac and tvOS builds), running
carthage build is much slower than
pod install - there are a few reasons for this:
- Carthage builds each framework for the target platform and simulator and bundles them together, rather than downloading and integrating the files into new targets on your Xcode workspace. On the plus side, this does lead to much faster build times when building the Xcode project itself.
- In my case, my app used RxSwift. In CocoaPods, I was only bundling RxSwift. Carthage on the other hand also builds and bundles RxCocoa, RxTests, RxBlocking, and all of the other subcomponents of RxSwift.
Unfortunately, this did mean to a significant increase in build time on my continuous integration service, Bitrise. After replacing the CocoaPods build step with the Carthage build step (which I really do appreciate being provided), I haven't been able to get a successful build under my free plan as the build and tests exceed maximum permitted time of 600 seconds. I may be wrong here, but if there was an option for Carthage to build frameworks just for the simulator, I reckon the build frameworks phase would be much faster for Bitrise and get my builds passing on there again. My current workaround is to ensure tests are run before submitting, which isn't hard to do given that I use Fastlane when submitting my apps.
Anyway, with my minor implementation points changed and all of the relevant setup and some final testing done, I submitted my latest app to Apple. The resulting IPA was smaller than before (by 10-20MB), so submission time was slightly shorter. And the app was uploaded all in one go with no rejections from Apple's automated checks! The app passed Apple's review and the rest, as they say, is history!
So would I use Carthage again? If my app has a native watchOS component, I would without hesitation! Where I would still use CocoaPods for the time being is for much "easier" projects where there's just main app, tests and UI test as targets. Long story short, I would use the right tool for the job! However, cocoapods.org itself will still be insanely useful for library discovery, and its quality metrics are so valuable when deciding on third party libraries! Carthage prides itself on being decentralised, so it doesn't have an equivalent: this makes seeing a "Carthage compatible" badge on a library's GitHub page insanely valuable when using Carthage.
All that said, there is one third package manager under active development, which I think will dominate in the future. It is Apple's very own Swift Package Manager! I haven't yet given this a spin myself, but I would like to one day in the future and I'm excited to see where it goes in the next couple of years!