Modular iOS

How to build universal iOS frameworks for distribution

Learn how to build an iOS framework that works with devices and simulators

Compiling iOS frameworks for distribution is not a straightforward task. Out of the box Apple offers no option for distributing compiled frameworks. Through Xcode user interface the option to archive a framework is disabled.

What about when you run Product > Build option?

Xcode builds the framework but only for either devices or simulators. But not both.

So how can you build an iOS framework that is compatible for both devices and simulators?

In this post we learn how to build iOS frameworks that work for both simulators and devices. These are known as universal iOS frameworks.

I assume you are familiar with the basics of Swift. Additionally you must also be familiar with the basics of iOS frameworks.

Let’s dive in!

I have used Swift 5.2 and Xcode 11.4.1 for this post.

How to build universal iOS frameworks

In this post we’ll cover the steps to build universal iOS frameworks. We’ll be starting off from a project that already contains a framework. We’ll then build a universal iOS framework. Finally learn how to consume a universal framework.

The steps we’ll take to in this post are:

  1. Retrieve starter pack
  2. Building a universal iOS framework
  3. Consuming the framework

1. Retrieve starter pack

Let’s start by downloading the starter Xcode project. Open terminal and execute the following commands:

cd $HOME
curl https://github.com/anuragajwani/ios-universal-framework/archive/starter.zip -o ios_framework.zip -L -s
unzip -q ios_framework.zip
cd ios-universal-framework-starter/MyFramework

The starter pack contains a framework project and an app project. The framework contains a single Login screen. The app currently display a Login button that doesn’t do anything. In the last step we’ll present the Login screen from the universal iOS framework when the user taps the login button.

2. Building a universal iOS framework

Next we’ll build a universal framework from the framework project. We’ll be using the command line exclusively to do so.

First let’s create a directory where we’ll output the frameworks. We’ll be building a framework that works for simulators, a framework for devices and finally we’ll merge these two into one universal framework. Execute the following command:

mkdir build

Next we’ll build the framework for devices. Run the following command:

xcodebuild clean build \
-project MyFramework.xcodeproj \
-scheme MyFramework \
-configuration Release \
-sdk iphoneos \
-derivedDataPath derived_data \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES

The above command will output a devices compatible framework under derived_data/Build/Products/Release-iphoneos/ within the project. The command is similar to when we build the framework through the Xcode user interface with the device destination set to Generic iOS Device.

Note BUILD_LIBRARY_FOR_DISTRIBUTION=YES argument. This sets the Build Libraries for Distribution to Yes. You can also set through the Xcode UI.

Built frameworks already contain a public interface in a serialised binary format. This format is subject to change in newer Swift versions. This means that future versions of Swift might not be able to read the public serialised binary format interface. Setting BUILD_LIBRARY_FOR_DISTRIBUTION to YES tells Xcode to generate a non-binary format of the frameworks public interface. This public interface is compatible with newer versions of Swift.

Let’s copy the framework into our output build folder. Execute the following commands:

mkdir build/devices
cp -r derived_data/Build/Products/Release-iphoneos/MyFramework.framework build/devices

Next we want to add simulator support to our build to make it a universal framework. Let’s build the framework for simulators. Execute the following command:

xcodebuild clean build \
-project MyFramework.xcodeproj \
-scheme MyFramework \
-configuration Release \
-sdk iphonesimulator \
-derivedDataPath derived_data \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES

The command above will output an iOS framework compatible with simulator s in derived_data/Build/Products/Release-iphonesimulator/.

Next let’s copy the simulator framework into our build folder. Execute the following commands:

mkdir build/simulator
cp -r derived_data/Build/Products/Release-iphonesimulator/ build/simulator/

Next we’ll create a universal framework. For this we’ll use the devices framework as our starting point. Execute the following commands:

mkdir build/universal
cp -r build/devices/MyFramework.framework build/universal/

Next we’ll extract the the simulator and devices binaries from the executables within the frameworks and merge into a single executable with both binaries. Execute the following command:

lipo -create \
build/simulator/MyFramework.framework/MyFramework \
build/devices/MyFramework.framework/MyFramework \
-output build/universal/MyFramework.framework/MyFramework

To make universal framework work we’ll also need the public interfaces for the binaries to be included in the framework. These are located under the directory .swiftmodule. As we based off the framework from device compatible framework devices interfaces are included. Let’s copy in the public interfaces for simulators. Execute the following command:

cp -r \
build/simulator/MyFramework.framework/Modules/MyFramework.swiftmodule/* \
build/universal/MyFramework.framework/Modules/MyFramework.swiftmodule/

And that’s it! Our framework is now ready to be consumed by devices and simulators!

3. Consuming the framework

For the final step we’ll learn how to consume the universal iOS framework and see it build and run for both iOS devices and iOS simulators. We’ll do that through the app included in the starter pack.

Let’s open the app project included in Xcode. The app project is included within the starter pack. Execute the following commands:

open -a Xcode ~/ios-universal-framework-starter/UniversalFrameworkDemo/UniversalFrameworkDemo.xcodeproj

Next we’ll link the framework from universal build folder into UniversalFrameworkDemo app. Let’s open project navigator. From menu select View > Navigators > Show Project Navigator.

Next let’s drag and drop the generated framework into the UniversalFrameworkDemo app project. Let’s open finder showing the generated framework. Execute the following command:

open -a Finder ~/ios-universal-framework-starter/MyFramework/build/universal

Next drag and drop MyFramework.framework into the project navigator in UniversalFrameworkDemo app.

When prompted “Choose options for adding these files:” check Copy items if needed and check UniversalFrameworkDemo target. Click Finish.

One last thing before the framework is successfully linked is to make sure that framework gets embedded into the app when the app gets built. On the project navigator select UniversalFrameworkDemo project settings (the one with the blue icon), then select the UniversalFrameworkDemo app target. Next select the General tab and finally scroll down to the “Frameworks, Libraries, and Embedded Content section.

Make sure the “Embed” is set to “Embed and Sign”.

Next let’s present MyFramework’s LoginViewController when the user taps the Login button on UniversalFrameworkDemo app. Open ViewController.swift. Add the following line under import UIKit:

import MyFramework

Then within loginButtonTapped present the LoginViewController from MyFramework:

let loginViewController = LoginViewController()
self.present(loginViewController, animated: true, completion: nil)

Finally select any device or simulator and the click Product > Run. You should now see the app running and presenting the login screen when login button is tapped regardless of the app being ran on simulator or device.

Summary

In this post we learnt:

  • how to build universal iOS frameworks
  • how to consume universal iOS frameworks

Final notes

You can find the full source code in the link below:

So you know how to create, maintain and compile iOS frameworks that are easily consumeable by iOS app projects. But how do you distribute these?

As you release newer versions of your framework integrators will need to remove the old version, download the new version and link the new version. They will need to know when a version is out too.

But worry not! There are great tools that manage framework integration where integrators can easily upgrade a version of your framework with the latest bugfix or minor version as desired by them. The two most popular tools are Cocoapods and Carthage.

In a previous post I covered how to distribute compiled iOS frameworks through Cocoapods. You can learn more by clicking the link below:

Stay tuned for more on iOS development! Follow me on Twitter or Medium.

Senior iOS Engineer @ Onfido. Writing weekly blogs on iOS and programming. Follow me to stay tuned!

Get the Medium app