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

Anurag Ajwani

--

Photo by Guillaume Henrotte on Unsplash

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).

Icons provided by icons8.com

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:

  1. Compiled static frameworks
  2. 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:

  1. Download the starter project
  2. Build the static library for simulators and devices
  3. 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:

  1. Static library (.a file)
  2. 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:

  1. Import XCFramework to iOS app
  2. 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:

  1. $(PROJECT_DIR)/MyStaticLib.xcframework/ios-arm64
  2. $(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.

Stay tuned! Follow me on Twitter or Medium.

--

--

Anurag Ajwani

Senior iOS Engineer at Travelperk. 7+ years experience with iOS and Swift. Blogging my knowledge and experience.