Swift Package Manager builds iOS frameworks
Swift Package Manager doesn’t work with iOS. That’s probably all you can say about the current state of SPM, but insomnia forced me to expand on the issue and compile the following essay.
- Yes, you can build iOS frameworks with Swift Package Manager;
- Yes, you can adjust settings via
.xcconfigand reuse generated
.xcodeprojin a real iOS application without 3rd party tools to parse a project structure, scripts on Ruby, etc.;
- Yes, you can use swift build to build your package but only as a library (but with some additional limitations);
- No, you cannot use swift test because you have to spawn a simulator to run them.
Current State of Swift Package Manager
iOS support is a hot topic in the Swift Package Manager community, and from time to time someone raises this same question again but the following comments sum up the community’s reactions (full thread on forums.swift.org):
I think that this will be best provided by native IDE integration. However, in the meantime, I’d welcome contributions to help improve Xcode’s project generation support.
The last thing I want from a platform-agnostic open-source package manager is built-in integration with a single-platform commercial closed-source IDE. :confused: I think this should be done independently by the DT team, without any special favouritism by SPM.
And you know what? I agree with these statements. Especially after some research. In general, SPM is a very ‘young’ and inflexible project. There are a lot of limitations, especially around generate-xcodeproj options of the swift package tool. And this is understandable given that Swift is a language, and all related tools need to be platform-agnostic as far as possible. Yeah, iOS is the biggest Swift consumer, and Apple contributes to Swift mostly because of iOS, but…
It’s going to be almost impossible to grow Swift into a mature technology if you’re limited and restricted by Xcode / iOS specific things / etc. And, it seems, this is the primary goal for Swift to remain just be a language. The fate of Objective C is a good example of why the Apple and Swift’s communities are trying to be agnostic. They are trying to build something big, and lack of iOS support is the price (among many others) at the moment. :kiss:
Having said this, we have exciting news about SPM and iOS friendship. SE-0236: Package Manager Platform Deployment Settings is accepted with some modifications. And the implementation of this proposal will help a lot to move forward in the case of iOS. The main goal is clear and straightforward:
Packages should be able to declare the minimum required platform deployment target version. SwiftPM currently uses a hardcoded value for the macOS deployment target. This creates friction for packages which want to use APIs that were introduced after the hardcoded deployment target version.
Why will it completely fail to solve the problem with iOS? Just read the “This option doesn’t resolve these problems” section.
Xcode 10.2 Beta
SE-0236 was accepted and implemented in Swift 5, and Apple shipped it with the latest Xcode 10.2 beta. This means that you can specify an iOS as a deployment target for your packages with a simple line in your
See an example in
xcode_10_2_beta branch in the example repository. It is still a beta implementation, and you will have a lot of issues with
build command, and run doesn’t work either, but it is a step toward eventual iOS support.
I hope that Apple will announce something good at WWDC’19. A new IDE for Swift, perhaps? Or open-sourced
xcodebuild replacement based on
llbuild? We’ll see. The current state is as complicated as it can be. We are trying to inject open-sourced platform-agnostic tools into a legacy (?) world of Xcode, and this problem can only be solved in one of two ways: either iOS specific build tools go open source, or Xcode team supports Swift infrastructure.
Is it possible to build an iOS framework with SPM? Yes! Absolutely!
Search for solutions for building iOS frameworks with SPM on DuckDuckGo and you will find some instructions (1, 2). But all of them have this one step that I hate:
sudo gem install xcodeproj :disgusting:. Can we do any better? Let’s give it a try.
First of all, let’s generate a template:
Now we have to convince SPM that we want an iOS project when it generates
xcodeproj. How? With
xcconfig, of course. Create a file
ios.xcconfig and add it to
./Sources folder. For example, let’s start with a basic version:
Looks good. Let’s see what SPM thinks of it:
Note: You do not have to specify a custom
.xcconfig file if you are using Xcode 10.2 Beta with Swift 5 Toolchain. Check the branch for more details.
It works! Let’s celebrate! But nope. We’re not there yet, need to dig deeper. Let’s check how the ‘Unit Tests’ target works, for example:
It doesn’t. Due to this strange and suspicious
XCTestCaseEntry. What is this? According to the swift-corelibs-xctest source code:
This is a compound type used by
XCTMainto show tests to run. It combines an
XCTestCasesubclass type with the list of test case methods to invoke on the class.
typealias looks like this:
Why doesn’t it work? For the same reason:
CoreLibs XCTest only supports desktop platforms
Thanks go to @larryonoff for his work on multi-platform support. But unfortunately we still can’t use it for our needs. Join this Add Unit Testing Infrastructure thread if you want to learn more about the current state of
swift-corelibs-xctest. We will move on and apply the fix to
&& !os(iOS)? Let’s keep going. Run Tests again and… We got what we need.
Then I created a simple iOS
ExampleApp and added the generated
xcodeproj as a dependency. Of course, I’ve added some iOS specific code to the framework:
And then reuse it from the example app:
The full example is available on GitHub. Thanks to
CLANG_MODULES_AUTOLINK, all iOS frameworks will be linked automatically. I didn’t try more complex scenarios (where one iOS module depends on another, etc.) because it’s not my goal here. But, in general, it does just work albeit some limitations. SPM doesn’t set up our xcconfig for some targets, and you have to include the SPM-generated
.xcodeproj to your
.xcodeproj, but all these tradeoffs seem reasonable in the interests of this research and achieving our current goal.
Back to Swift Package Manager
See. We can do better 🥳. But we forgot about SPM during this Xcode journey. Let’s close our fancy, dark-themed Xcode, open Terminal and run
swift build for our iOS’ish package (I’m going to use the package from the example project mentioned above):
no such ‘UIKit’ module. Can we do better? I doubt it, but let’s try. First of all, we have to find out where SPM gets all these environment variables from. We can ask it with
swift build — verbose:
Nice. No smoke or mirrors. Let’s try to changing some
swiftc options to build the project against proper
Looks better. We built the binary:
Let’s carry out some inspections, just to make sure that everything is OK:
lipo is a bit useless in this case because we were building for a simulator, but
nm shows us everything we need to know — iOS frameworks symbols are available. Unfortunately,
swift build doesn’t produce
.framework by default. I think it’s doable even in this case but let’s look at that ‘next time’.
Epilogue: swift test
And the final call. We already have one unit-test for our package: it uses
UIKit, and I would rate this experiment as successful if we could run the test target with
swift test. It’s almost impossible though, because usually unit-tests for simulator have to spawn to a simulator process. I don’t even think that it’s possible for an actual iOS project. Anyway.
And there’s another problem.
.xctest bundle is compiled but
xctest tool gets confused with search paths:
swift build &
test produce beneficial debug information and store it in
.build/debug.yaml with all previous options and arguments. This goes for the options for a module itself as well, so it’s time for our command line friends again:
As you can see, for some reasons, it tries to link UIKit.framework twice:
- via the
- and via the expected
Let’s check the information from the module itself with
otool, to be sure that
load is describing the correct framework to link:
Seems correct to me. So, to eliminate the confusion let’s pass the linking option directly with
but built for simulator (not macOS)). This is my final conclusion, my friend. It doesn’t take that much effort to link proper frameworks, but it’s impossible to run these tests for iOS device or simulator without SPM support. It seems that not even
xctest are up to the job.
xcodebuild assistance is needed here. But enough of these weird logs and useless rants, let’s summarise.
Thanks for reading, first of all! So, what did we learn?
- We can use SPM in sporadic cases for iOS, just for integration, thanks to
- The future of the friendship between iOS and SPM is unsure even with this SE-0236 proposal;
- Swift build and test helpers are useless to us without iOS-specific features and full Xcode support. And that’s unlikely to happen. Of course, SPM can be integrated on top of Xcode by Xcode team. For example, they will extend
xcodebuildfunctionality. But concerning SPM, it is going to be a different story.
Regarding SPM. Generally, I think it’s doable, and we can improve SPM to support any platform you like. My best advice at the moment is to introduce pipeline plugins for SPM, enabling you to transfer the control flow to a separate tool, with expected input and output. Something like Xcode custom build phases but is smarter and more flexible. It will allow SPM to be platform-agnostic as now, but the Xcode team can create a plugin for the whole iOS flow support. Or Uber. Or Google. Or me. Whatever.
Stay tuned and hydrated!