Accio – SwiftPM for iOS & Co. today!
Introducing a SwiftPM-based dependency manager for the Apple developer community to fill the gap until integration into Xcode.
Note: Feel free to skip ahead to the section “A new Dependency Manager is born” if you are already experienced with Carthage, CocoaPods & SwiftPM.
Table of Contents
- A little History of Hope (Motivation)
- Comparison with the Status Quo (Rationale)
- A practical Approach for fast Adoption (Implementation)
- A new Dependency Manager is born (Accio Introduction)
- Installing Dependencies with Accio (Accio Usage by Example)
- Multi-Layered Caching to minimize build times (Main Advantage)
A little History of Hope
When Swift Package Manager was announced back in November 2015, the Apple developer community began hoping for Xcode to gain support for fetching and integrating open source frameworks automatically sometime soon. Then came WWDC 2016 without any such support. But it was early days and the project itself was very much in flux – so was Swift. Therefore, many developers kept their hopes growing for the next WWDCs.
When 2017 and 2018 both also disappointed in this regard, it became somewhat clear that the current direction of Swift Evolution wasn’t targeting Apple platforms yet. Given that CocoaPods and Carthage were already available as dependency managers for those platforms, they seemed not to put any pressure into targeting iOS projects.
That didn’t mean SwiftPM gave up on that goal though, nor has the community ever stopped to try using it. There were several approaches of using SwiftPM for iOS development, including this recent guide on GitHub. But all of them were quite cumbersome and error-prone. Nevertheless, Swift Evolution proposals like SE-236 continue keeping the hopes up for the community that Apple will sometime integrate SwiftPM naturally into our development workflows.
Searching for official statements, in SwiftPM’s README we can find a link to this comment on the Swift Forums from Rick Ballard. It’s basically stating that official plans from Apple are kept secret but SwiftPM should lay the groundwork for such a feature to be possible. But clearly missing features for it like SE-2866 and SE-3948 are still not implemented after more than 2 years of creation, so it’s not clear when the time will come. My personal hope is that we will see a first official foray into dependency management by Apple at WWDC 2020. But who really knows?
Comparison with the Status Quo
As mentioned above, CocoaPods and Carthage are two well-established dependency managers the Apple developer community can decide between. But what could be the advantages over CocoaPods and Carthage when SwiftPM one time supports building for iOS & Co. and is even integrated with Xcode? Or to put it differently: What are the downsides of CocoaPods and Carthage which could be improved upon? Let’s make a quick comparison:
CocoaPods is written in Ruby and was first developed in 2011. It uses two different manifest formats called “Podspec” (for framework authors) and “Podfile” (for app developers) which are inspired by the two most popular dependency managers in the Ruby community. While it’s possible to link to dependencies via Git repo URLs, they also provide a companion website on CocoaPods.org where official releases can be made by framework developers and frameworks can be discovered by app developers via search. Therefore CocoaPods can be considered a centralized depencency manager.
In order for a framework to support CocoaPods, it needs to provide a “Podspec” file including information about its main developer(s), its license, the project structure and the supported platform versions. App developers need a “Podfile” where the dependencies can be specified including their version requirements and how they should be integrated, all written in basic Ruby. CocoaPods supports both integration by code and via dynamic frameworks. To link everything correctly, CocoaPods creates an Xcode Workspace and manages that automatically — developers need to use that new file instead of the normal Xcode project file.
Carthage is written in Swift and development began back in 2014 shortly after the release of Swift 1.0. Its manifest is a custom OGDL conforming format called “Cartfile”. It’s used both by framework authors and app developers alike — a second file with the suffix “.private” can be used by framework authors for dependencies only needed for development & testing purposes. The Cartfile is not required for frameworks though, as long as they don’t have any subdependencies. Shared Xcode schemes are used to determine what to build. Carthage has no accompanying website, all dependencies are linked via Git repo URLs (or local paths), so it can be considered a decentralized dependency manager.
The Cartfile format is very restrictive compared to CocoaPods in that it doesn’t provide any information about its main developer(s), its license, the project structure or the supported platform versions. It’s basically only a list of dependency paths with version requirements. Carthage also doesn’t do any automatic linking, users need to link the Carthage-built dynamic frameworks manually within their projects. Also, some additional initial configuration within Xcode is needed to work around issues with App Store submission.
Both, CocoaPods and Carthage can be configured to build only the framework targets needed for the required platform(s). Both support downloading pre-built binaries when provided by framework authors. Also both can be configured to reuse cached builds of frameworks within a project.
Carthages advantages over CocoaPods are that it’s unintrusive by only running
xcodebuild commands and by being written in Swift, so being more accessible for improvements by the community (note though, that it’s written using ReactiveSwift which might hold off many developers). Another advantage for framework authors is, that it’s easier to support — but an Xcode project with a shared scheme is required.
CocoaPods advantages over Carthage are it’s automated integration, saving many manual steps for developers, its better discoverability and its support for modularization of a framework into multiple choosable modules. Carthage solves such situations by always building all available modules regardless of which are actually needed, leading to unnecessary waits.
SwiftPM one day, in my opinion, should combine all the advantages from above and even provide some completely new functionality:
Both, CocoaPods and Carthage don’t have integrated mechanisms for reusing cached builds of frameworks across multiple projects, leading to unnecessary build times. Both are not written in classic object-oriented Swift with some functional additions to be accessible to nearly the entire community. Also both have their custom manifest formats, not written in Swift, not discussed and reviewed in a structural manner by the Swift community.
A practical Approach for fast Adoption
We all know SwiftPM is gonna take a while to officially support all those features. But maybe there is a way we can use what SwiftPM already offers today and fill the gaps with the alternatives we have today? How would that look like? Would it be better than using CocoaPods or Carthage alone?
Let’s see what SwiftPM offers today:
- SwiftPM already has a relatively flexible manifest format, much more flexible than Carthages format. Not perfect yet, but it already supports modularity and minimum platform requirements, to name two.
- SwiftPM has dependency resolution implemented already. Also, it has a
show-dependenciessubcommand which can be configured to use JSON as its output format so we can print out the resolved dependency graph.
- SwiftPM can also create an Xcode project with a shared scheme with a framework target which can be easily pointed towards iOS, tvOS, macOS or watchOS platforms by tiny changes.
What main parts are missing today? And could we fix them? Let’s see:
- SwiftPM can’t build a product that can be directly used in e.g. iOS projects.
Fix: We could use
xcodebuildwith a generated Xcode project.
- SwiftPM doesn’t support including resources like images with targets.
Fix: We could reuse the existing Xcode projects already provided within frameworks which support Carthage.
Now, seeing the most apparent missing parts, isn’t it the obvious solution to keep it simple and use Carthage for the building task entirely? Carthages
build command already executes
xcodebuild for us, so it would fix both problems at once. But would we circumvent all of Carthage’s disadvantages by using SwiftPM for dependency resolution and for the manifest format?
Let’s check the disadvantages of Carthage:
- Automated Xcode Integration: Unfortunately, this would still be missing.
- Framework Discovery: SwiftPM lists this in their Evolution Ideas, but correctly states: “This will be a large project.” — So, a “No” here, too.
- Modularization Support: Fixed, SwiftPM supports modularization!
So, we can see that at least one disadvantage would be solved. The framework discovery feature is a nice-to-have, but can be considered at a later time. Also it should probably be done in a way discussed and accepted by the community. The real missing part here which needs to be fixed is the automated Xcode integration. So when taking this pragmatic approach of using SwiftPM for dependency resolution and Carthage for building the resolved and checked-out frameworks, we only need to add Xcode integration automation ourselves to use SwiftPM today … that sounds solvable!
A new Dependency Manager is born
Let me introduce you to Accio (read “AH-kee-oh”), a new SwiftPM based dependency manager that basically does just what was described above and additionally also provides some features that neither Carthage nor CocoaPods have. Before we get to them though, let’s first have a look at how to use it.
First, we need to install it with these two commands:
brew tap JamitLabs/Accio https://github.com/JamitLabs/Accio.git
brew install accio
Note that both Xcode 10.2 command line tools (they include SwiftPM) and Carthage need to be installed for Accio to work, so install them if necessary.
Next, let’s create a new iOS project, e.g. using “Single View App”:
The result is a basic buildable iOS app project structured like this:
Build and run the project to ensure it’s building properly and running without any crashes. Having that on our checklist, let’s add some dependencies. In order to do that, we need a
Package.swift manifest file, let’s create it!
Accio provides an
init command that creates a
Package.swift file for us which is pre-filled with a basic skeleton structure. In order to use it, we need to specify the name of our Xcode project file and the name of our apps target. In this example, both are
Demo, so we could initialize the manifest file like so:
accio init --project-name "Demo" --target-name "Demo"
Note though that the
--target-name argument accepts multiple targets, too. We just need to separate them with a
,, so let’s add our unit test target, too:
accio init --project-name "Demo" --target-name "Demo,DemoTests"
The result is a
Package.swift file with the following contents:
Note how Accio automatically guessed that
DemoTests is probably a
.testTarget and Demo is a plain
.target. Also, Accio placed the following two lines into the
.gitignore file to prevent committing temporary files (placed under
.accio) and build products (placed under
In a real project, the values set for the
path parameters might be wrong, so make sure to check them. For this Demo example the defaults are fitting.
Next, let’s add our first few dependencies. To showcase what kind of libraries Accio supports, let’s try the most popular Swift framework (Alamofire) and the most popular Obj-C framework (MBProgressHUD). Since both don’t have any subdependencies, let’s also add Moya to the game, especially given it has three different variants (Moya, RxMoya and ReactiveMoya):
Note how for each dependency we needed to both add the dependencies source into the
dependencies parameter list (lines 8–15) and also the names of the libraries we want to link to the targets
dependencies (lines 21–23). For this example, we opted for the plain
Now that we have specified our dependencies, let’s install them using Accio.
Installing Dependencies with Accio
Accio provides two separate commands for installation:
update command always checks if newer versions of dependencies within the specified ranges are available and updates them if needed. The
install command on the other hand only updates if there is no
Package.resolved file yet, meaning that neither of the two commands were ever successfully executed before. As we have never installed dependencies yet, both commands will do the same thing.
But before running any install commands, we should close our project if it’s open in Xcode. This will prevent any issues with Accios automatic Xcode integration feature, do you remember it? Okay, now simply run:
As of the time of this writing, this will result in an error like this (shortened):
✨ Updating dependencies ...
https://github.com/jdg/MBProgressHUD.git has no manifest for v1.1.0
Error: 'swift package update' returned with error code 1.
As the error reads, at this time
MBProgressHUD has no
Package.swift manifest file yet. But remember how Accio currently only uses the manifest file for dependency resolution? Since we don’t need the project to actually build with SwiftPM, we just need a properly configured
Package.swift file. And adding one is very simple — the README of Accio has a detailed section with instructions and manifest examples for copy & paste purposes. Check it out!
MBProgressHUD is one of the many popular projects where support for Accio is even part of its integration tests though, there’s already a fork and a pull request which might be merged already by the time you’re reading this (rendering the next change unnecessary). So, let’s use the fork instead:
.package(url: "https://github.com/AccioSupport/MBProgressHUD.git", .branch("master")),
Note that we also changed
.branch. For a full reference of all available options, read this section in the official manifest docs on GitHub.
Now with the fix, let’s try installing the dependencies again:
Your output now should look something like this (shortened):
✨ Updating dependencies ...
Fetching https://github.com/Alamofire/Alamofire.git [...]
✨ Resolving dependencies for target 'Demo' on platform 'iOS' ...
*** Building scheme "Alamofire iOS" in Alamofire.xcworkspace
*** Building scheme "MBProgressHUD" in MBProgressHUD.xcworkspace
*** Building scheme "Result-iOS" in Result.xcodeproj
*** Building scheme "Moya" in Moya.xcodeproj
✨ Copying build products of target 'Demo' into 'Dependencies' ...
✨ Adding frameworks ["Alamo...] to project navigator group 'Dependencies/Demo' & linking with target 'Demo' ...
✨ Creating new copy build script phase 'Accio' for 'Demo'...
✨ Updating paths in build script 'Accio' for target 'Demo' ...
✨ Successfully updated dependencies.
Note how Accio did automatically detect that our project is targeting the iOS platform and only built schemes for that platform, skipping unnecessary schemes like
Alamofire tvOS. Also it correctly installed necessary subdependencies like
Result but automatically skipped unnecessary schemes like
Okay, now that the output looked good, let’s open our project and investigate:
Note how Accio automatically added a new group named
Dependencies to the project navigator and linked the specified frameworks with the
Demo target. Also it added a new build script to the demo target to ensure Carthage’s six-steps long configuration process is handled by the install command automatically. Okay, so what’s the next step?
There is none! If all dependencies have support for Accio, specifying them in the manifest file and running the install command is all you need to do. We have now successfully integrated some dependencies to our iOS project and could start using them right away — building and running will just work!
For example, we could update our
ViewController.swift file to this code:
Now, when we build and run our app, the result looks like this:
Everything is working as expected! 🎉
Next, let’s say we want to add a framework for testing purposes only, for example the flexible SnapshotTesting library from pointfreeco:
Then we just re-run
accio update, wait for it to finish and reopen the project:
Accio did the right thing again, it linked the framework only against the test target and also used a “Copy Files” phase instead of the copy script. If you have never heard of the difference of this Carthage configuration detail before, well you won’t need it any more with Accio anyways, so just don’t bother.
Multi-Layered Caching to minimize build times
In the above command outputs several parts were removed. The first time we installed our dependencies, the output actually included the following lines:
✨ Saved build products for Alamofire in local cache.
✨ Saved build products for MBProgressHUD in local cache.
✨ Saved build products for Result in local cache.
✨ Saved build products for Moya in local cache.
Without any configuration, Accio by default saves all successfully built build products to a device-local cache, which resides in the macOS cache folder:
This way, when we re-ran
accio update after adding test dependencies, the outputs actually included the following lines:
✨ Found cache for Alamofire in local cache - skipping build.
✨ Found cache for MBProgressHUD in local cache - skipping build.
✨ Found cache for Alamofire in local cache - skipping build.
✨ Found cache for Result in local cache - skipping build.
✨ Found cache for Moya in local cache - skipping build.
✨ Saved build products for SnapshotTesting in local cache.
This means, already built frameworks are reused instead of rebuilt each time. A build product within the cache is uniquely identified by a combination of the library name, the commit hash, the Swift version and the target platform.
For example, the cached build products path for Alamofire from above is:
This local cache ensures that if a specific commit of a framework was already built for one project, it can be reused for other projects on the same machine. This already cuts compile time quite a lot if you work within several projects.
But Accio goes even a step further: It also provides an optional shared cache! By simply adding the option
--shared-cache-path (or the shorthand
-c), you can specify a custom path which could be shared with your entire team:
accio update -c '/Volumes/GoogleDrive/Team Share/AccioSharedCache'
By using a cloud file service like Google Drive and creating a folder for your shared cache files there, you can cut the dependencies build times of the entire team drastically. Only the first person building a specific commit or version of a framework will wait, everyone who follows will just hit the cache!
There’s even an option to use a shared cache by default. Just run this once:
accio set-shared-cache '/absolute/path/to/your/shared cache'
This will create a
config.json file under
Application Support/Accio with your chosen path. That path is user-specific, so other users on the same machine won’t be affected. To revert it, just remove the
config.json file. To change your shared cache path, just re-run the command with your new path.
Having a local and a shared cache is already a great improvement over both Carthage & CocoaPods. But there are even plans to go yet another step further and provide an optional global cache, where basically the most popular frameworks would be provided as prebuilt frameworks, similar to Homebrew. But that’s not implemented yet and might also take a while. It could be especially useful for CI builds once implemented, where configuring a shared cache might be a hurdle depending on the chosen service.
By using the new dependency manager Accio, you can profit from SwiftPM today and make it work with iOS & Co. without any hassle. It uses Carthage for building at the moment, therefore adding support for Accio is very easy. But it doesn’t share Carthage’s disadvantages — it not only fixes them, but it even provides the best caching system available — allowing you and your team to say ‘Good Bye’ to probably about 90% of dependency build times! 🍻