Modular iOS
How to build universal iOS static libraries using XCFramework
Learn how to build static libraries for simulators and devices in a single artefact
Compiled static libraries are not easy to integrate with. A build only supports iOS simulators or devices but not both. To allow integrators use the static library for simulators and devices you must provide separate builds for each target destination. Integrators will need to switch to the correct build variant based on the target destination.
Furthermore an integrator also needs to configure their app’s build setting to consume the static library. At the very least an integrator need to tell Xcode where the public interface of the static library is located. That is how Xcode knows what code is consumable from the static library.
Static libraries public interfaces (.swiftmodule
directory) are distributed separately from the static library (.a
file).
So the integration experience with static libraries is cumbersome. But is there a way to distribute a single artefact which supports simulators and devices? There are 2 ways:
- Compiled static frameworks
- Using XCFrameworks
The first is hacky method (not supported by Apple) to distribute static libraries that look like frameworks and suuport both iOS devices and simulators. The second is using a new format that supports hosting both build variants in a single artefact and supported by Apple. In this post we’ll be focusing on the latter.
In this post I will first cover what XCFramework is and why use them. Then I will show you how to build a universal iOS static library in an XCFramework form. Finally I will show you how to consume the XCFramework.
I assume you are already familiar with static libraries, compiled static libraries, basic iOS and Swift.
What is XCFramework?
XCFramework is a structured directory that can contain multiple build variants of the same module — static library or dynamic framework. At least it needs to contain one build variant.
A build variant is a module transformed from source code (Swift, Objective-C, etc…) into computer instructions or binary for a specific platform and CPU architecture.
For example iOS can run in simulators on macs which speak in a language (x86_64
) different from iOS on iPhones and iPads (arm64
). In fact iPhone 4s CPU’s and older spoke a different language (armv7
) than newer iPhones (5s and later).
Build variants can include:
- iOS simulators — x86_64
- iOS devices — arm64
- iOS devices — armv7
- tvOS simulator — x86_64
- tvOS devices — arm64
The list above is not exhaustive. For this post we’ll only be concerned with iOS devices arm64
architecture, which covers all iPhones sold in the market over the last 5 years including the ones on offer, and iOS simulators so integrators can test out your static library whilst developing their app.
Why use XCFramework?
As already mentioned XCFramework wraps multiple build variants of the same library and their interfaces into a single artefact. This eases the integration experience for consumers of your static library as they don’t need to switch between variants when they change the target destination from iOS devices to simulators or vice versa.
How to build universal static library using XCFramework
For this section we’ll be using an already existing static library project. We’ll build two variants — one for devices and another for simulators. Finally we’ll wrap the two build variants into an XCFramework.
The steps we’ll take:
- Download the starter project
- Build the static library for simulators and devices
- Generate an XCFramework using the compiled static libraries for simulators and devices
We’ll run all of the steps in this section in in the Terminal app. Before proceeding open the Terminal app.
I have used Swift 5.2.4 and Xcode 11.5 whilst writing this article.
1. Download the starter project
Let’s download the starter pack. Execute the following commands:
cd $HOME
curl https://github.com/anuragajwani/ios-static-lib-xcframework/archive/starter.zip -o static_lib.zip -L -s
unzip -q static_lib.zip
The starter pack contains an iOS static library project and an iOS app project.
2. Build the static library for simulators and devices
In order to create the wrapping XCFramework for the static library we must first build the static libraries; one variant for iOS simulators and another for iOS devices.
Let’s create a directory to store our build variants.
cd ~/ios-static-lib-xcframework-starter/MyStaticLib
mkdir build
First let’s build the static library for iOS simulators and store it in our build folder. Execute the following commands:
xcodebuild build \
-scheme MyStaticLib \
-derivedDataPath derived_data \
-arch x86_64 \
-sdk iphonesimulator \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES
mkdir -p build/simulators
cp -r derived_data/Build/Products/Debug-iphonesimulator/ build/simulators
The command above will generate the static library archive and interfaces for simulators and store them under build/simulators
.
Next we’ll build the static library for iOS devices. Execute the following commands:
xcodebuild build \
-scheme MyStaticLib \
-derivedDataPath derived_data \
-arch arm64 \
-sdk iphoneos \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES
mkdir -p build/devices
cp -r derived_data/Build/Products/Debug-iphoneos/ build/devices
The command above will generate the static library archive and interfaces for devices and store them under build/devices
.
This is what the build folder looks like now:
build
├── devices
│ ├── MyStaticLib.swiftmodule
│ │ ├── Project
│ │ │ ├── arm64-apple-ios.swiftsourceinfo
│ │ │ └── arm64.swiftsourceinfo
│ │ ├── arm64-apple-ios.swiftdoc
│ │ ├── arm64-apple-ios.swiftinterface
│ │ ├── arm64-apple-ios.swiftmodule
│ │ ├── arm64.swiftdoc
│ │ ├── arm64.swiftinterface
│ │ └── arm64.swiftmodule
│ └── libMyStaticLib.a
└── simulator
├── MyStaticLib.swiftmodule
│ ├── Project
│ │ ├── x86_64-apple-ios-simulator.swiftsourceinfo
│ │ └── x86_64.swiftsourceinfo
│ ├── x86_64-apple-ios-simulator.swiftdoc
│ ├── x86_64-apple-ios-simulator.swiftinterface
│ ├── x86_64-apple-ios-simulator.swiftmodule
│ ├── x86_64.swiftdoc
│ ├── x86_64.swiftinterface
│ └── x86_64.swiftmodule
└── libMyStaticLib.a
Each subfolder contains two items:
- Static library (
.a
file) - Public interfaces (
.swiftmodule
directory)
3. Generate an XCFramework using the compiled static libraries for simulators and devices
Next let’s wrap our static library builds into a single .xcframework
. For such we’ll need to use xcodebuild
. Execute the following command
xcodebuild -create-xcframework \
-library build/simulators/libMyStaticLib.a \
-library build/devices/libMyStaticLib.a \
-output build/MyStaticLib.xcframework
Above we have told xcodebuild
to create an xcframework using the two build variants. Notice we didn’t specify the location of the .swiftmodule
directory. However xcodebuild
does look for it within the same containing directory.
And that’s it! We have now generated an .xcframework
for our static library. You can find MyStaticLib.xcframework
under the build folder.
Notice that the .xcframework
contains two directories — one for each build variant.
MyStaticLib.xcframework
├── Info.plist
├── ios-arm64
│ ├── MyStaticLib.swiftmodule
│ │ ├── Project
│ │ │ ├── arm64-apple-ios.swiftsourceinfo
│ │ │ └── arm64.swiftsourceinfo
│ │ ├── arm64-apple-ios.swiftdoc
│ │ ├── arm64-apple-ios.swiftinterface
│ │ ├── arm64.swiftdoc
│ │ └── arm64.swiftinterface
│ └── libMyStaticLib.a
└── ios-x86_64-simulator
├── MyStaticLib.swiftmodule
│ ├── Project
│ │ ├── x86_64-apple-ios-simulator.swiftsourceinfo
│ │ └── x86_64.swiftsourceinfo
│ ├── x86_64-apple-ios-simulator.swiftdoc
│ ├── x86_64-apple-ios-simulator.swiftinterface
│ ├── x86_64.swiftdoc
│ └── x86_64.swiftinterface
└── libMyStaticLib.a
Each directory name has information on the compatible platform (iOS) and architecture (arm64
for iOS devices and x86_64
for macs) for which its static library and interfaces works for. The integrators app project knows which build to pick based on the target destination (device or simulator) and the name of build variant directory name.
How to consume the XCFramework
In this section we’ll learn how to consume the XCFramework that we generated in the previous section.
The starter pack that we downloaded in the previous section includes an iOS app project. We’ll be consuming the XCFramework in this iOS app.
The steps we’ll take:
- Import XCFramework to iOS app
- Consume code
1. Import XCFramework to iOS app
First let’s open the app project. Run the following command:
open -a Xcode ~/ios-static-lib-xcframework-starter/StaticLibXCFrameworkDemo/StaticLibXCFrameworkDemo.xcodeproj
Next open the directory containing the XCFRamework in Finder:
open ~/ios-static-lib-xcframework-starter/MyStaticLib/build
Let’s drag and drop MyStaticLib.xcframework
to Xcode.
When prompted “Choose options for adding these files:” make sure “Copy items if needed” and StaticLibXCFrameworkDemo
options are checked. Finally Click Finish.
One last thing before we can consume the static library is to set Import Paths build setting. We must provide the path to the directory where each and every .swiftmodule
directories are. This will allow the app to read into the code is available to it from the static library.
Open project configuration settings (the one that says StaticLibXCFrameworkDemo
with a blue icon to its left on the project navigator). Select StaticLibXCFrameworkDemo under targets in the editor area. Then select Build Settings tab. Search for Import Paths which lives under Swift Compiler — Search Paths.
Add the following two values:
$(PROJECT_DIR)/MyStaticLib.xcframework/ios-arm64
$(PROJECT_DIR)/MyStaticLib.xcframework/ios-x86_64-simulator
Now the xcframework is ready to be consumed.
2. Consume code
Let’s consume the code within MyStaticLib
.
Open ViewController.swift
. Before consuming the code of the static library we need to import it within the file we want to consume it. At the top let’s import the static library. Under import UIKit
add the following line of code:
import MyStaticLib
Now let’s consume the code. In the viewDidLoad
function add the following line of code:
functionA()
That’s it! Run the app in a simulator or device and watch the console to get the message from functionA()
in the static library.
Summary
In this post we learnt:
- what xcframeworks are
- how to build static libraries using
xcodebuild
- how to wrap build variants of the same compiled static library into an XCFramework
- how to consume an XCFramework
Final Notes
During the consuming of the XCFramework containing the static library we must set the Import Paths build configuration on the consumer app. I expected this step would be removed when XCFrameworks came out. It looks like the situtation has not changed with Xcode 12 beta at the time of writing.
You can now build an XCFramework from static libraries. But how to distribute it? As of Xcode 12 beta, Swift Package Manager now supports distribution of XCFrameworks. Also Cocoapods as of version 1.9.0 support XCFramework distribution. I will be covering both of these in future posts.