Modular iOS
Distributing compiled iOS Swift static libraries and Swift static frameworks
Great libraries are ones that are easy to consume. They must be easy to install and use. So how can we make Swift static libraries easy to install and consume?
The installation process depends on how we deliver our static library. There are 2 ways we can deliver a Swift static library:
- Deliver the source code to the integrator
- Compile it and then distribute the compiled library
This post will focus on the second case.
In this post we will learn:
- why should you distribute compiled static libraries
- how to build compiled static libraries using Xcode
- what are static frameworks and why you should distribute compiled static frameworks
- how to build compiled static frameworks
For this tutorial I expect that you have a basic understanding of what a static library is. If you are not familiar with it feel free to check out my post Reusing code and resources with Swift static libraries and resource bundles. Feel free to skip any sections if you are already familiar with it. The sections are consumable on its own.
I have used Swift 5.2 and Xcode 11.4.1 for this post.
Why should you distribute your static library compiled?
When delivering our library compiled we avoid having the source code delivered to our integrator. If your company has secrets in the source code it would rather not reveal to the world then delivering your library compiled is a way to hide your company's secret.
Another reason to deliver your library compiled is to save the integrator time from compiling your code. This is true even if aren’t too concerned about revealing your code.
Thus there are two main reasons to deliver your static library compiled:
- not reveal our source code
- save app compilation time
How to distribute compiled static libraries
In this section we’ll look into compiling our static library through the Xcode interface. We’ll then integrate it with an app to test our compiled Swift static library.
In this section we will:
- Download the starter project
- Build a compiled static library
- Create an app to consume the static library
- Link the static library to the app
1. Download the starter project
First let’s download the starter project. The starter project contains a Swift static library ready to be consumed. Open terminal and run the following commands:
cd $HOME
curl https://github.com/anuragajwani/compiled_swift_static_lib/archive/starter.zip -o compiled_swift_static_lib.zip -L -s
unzip -q compiled_swift_static_lib.zip
cd compiled_swift_static_lib-starter/MyStaticLib
2. Build a compiled static library
Next let’s open the static library project. Run the following command:
open -a Xcode MyStaticLib.xcodeproj
Once Xcode opens up select MyStaticLib
scheme from the Xcode toolbar and then select a iPhone simulator from the device list.
Next from menu select Product > Build.
Then open project navigator, from menu select View > Navigators > Show Project Navigator.
You’ll notice under MyStaticLib > Products folder a file named libMyStaticLib.a
. Right click on libMyStaticLib.a
and then select Show in Finder.
Finder will open up with the location of libMyStaticLib.a
.
Notice that finder also shows a folder in the build folder named MyStaticLib.swiftmodule
. We’ll come back to this later. Keep this window open for later convenience.
3. Create an app to consume the static library
Next we’ll create an app to consume our static library. Open Xcode and from menu select File > New > Project…
From project template choose Single View App.
On new project options name the product MyApp. Select the language as Swift. Select Storyboard as the user interface option. Keep all checkboxes unchecked. Click Next.
When prompted where to save the project hit Cmd ⌘ + Shift ⇧ + G keys. When prompted “Go to the folder:” enter ~/compiled_swift_static_lib-starter
and then click Go.
Finally click Create.
4. Link the static library to the app
Now we have an app to consume our static library. Next lets link our static library to our app. Drag and drop libMyStaticLib.a
into MyApp
project.
When prompted “Choose options for adding these files” check Copy items if needed. Select Create Groups for the added folders option. Check the MyApp target in the “Add to targets:” list. Click Finish.
Note the static library has now been added to the project and linked. You can check by selecting MyApp
project settings in the project navigator then selecting the MyApp
target in the editor pane. Next select General
tab and under Frameworks, Libraries, and Embedded Content
you should find libMyStaticLib.a
.
Next let's call a function from MyStaticLib
. Open AppDelegate.swift
. After import UIKit
add the following line:
import MyStaticLib
Then within the function in the AppDelegate class
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
add the following line before the return true
line:
functionA()
From the Xcode toolbar make sure to select MyApp scheme and a simulator device.
You’ll notice that Xcode complains about being unable to find MyStaticLib
module even though we have linked it to MyApp
.
The reason why Xcode is complaining is due to the fact that we have only linked the compiled code and not included the public interface to consume the compile code.
Remember earlier when we built MyStaticLib
? There were 2 artefacts built:
libMyStaticLib.a
fileMyStaticLib.swiftmodule
folder
The .swiftmodule
folder is the public interface for well … a swift module. That is for static libraries as well as dynamic frameworks. To make MyApp
’s integration with MyStaticLib
we must tell MyApp
to search for public interfaces where the swiftmodule
folder resides.
Drag and drop MyStaticLib.swiftmodule
into the MyApp
project directory in Finder
.
Next open MyApp
project in Xcode. Open project configuration settings (the one that says MyApp
with a blue icon to its left on the project navigator). Select MyApp under targets in the editor area. Then select Build Settings tab. Search for Import Paths which lives under Swift Compiler — Search Paths.
The current value is empty. Change this to $(PROJECT_DIR)
. Xcode will automatically change the path to the current project directory path.
Build and run the app in any simulator. This time it should have built and ran successfully 🎉. Watch the console (View > Debug Area > Activate Console) to get a response from MyStaticLib
.
Next let’s build for devices. From the Xcode toolbar select the MyApp scheme and for devices select Generic iOS Device.
Now from menu select Product > Build. You should see the following error in the issue navigator (View > Navigators > Shoe Issue Navigator):
Why do we get this error? We get this error because our static library binary format is not compatible for devices. Remember in step 2 when we built our static library targeted to simulators? That means that our static library was only built for simulators. To support device we would have to target Generic iOS Device. However the compiled static library would not work for simulators then.
Therefore compiled static libraries are:
- complex to integrate with
- hard to manage when changing the target device between physical and simulator device
So is there a way to make the integration experience better? Yes there is! The answer is Swift static frameworks! The title of the post already gave it away didn’t it? 😅
What are static frameworks and why should you distribute your compiled static library as a static framework
In the previous section when we compiled a static library:
- it was only compatible with devices or simulators, not both
- integrators required to manage a separate directory for the public interface of the static library
Apple does not really offer other mechanisms of distributing modules which are statically linked for iOS out-of-the-box. However there is a way for us to make integration experience better than the one offered by Apple. The solution is called static frameworks.
In essence static frameworks imitate the structure of dynamic frameworks. Dynamic frameworks allow integrators to have a single artefact that is compatible with both devices and simulators.
Knowledge of dynamic frameworks is not required for this post.
How to distribute compiled static frameworks
In this section we will learn how to compile static libraries into static frameworks. Then we will integrate with the static framework in our consumer app from the previous section; MyApp.
In this section we will:
- Download the starter project
- Build a static framework from MyStaticLib static library
- Integrate MyStaticLib static framework into MyApp
1. Download the starter project
First let’s download the starter project for this section. The starter project contains the sane Swift static library as the previous section. It also contains the consumer app that we created. Open terminal and run the following commands:
cd $HOME
curl https://github.com/anuragajwani/compiled_swift_static_lib/archive/starter-static-framework.zip -o compiled_swift_static_lib.zip -L -s
unzip -q compiled_swift_static_lib.zip
cd compiled_swift_static_lib-starter-static-framework/MyStaticLib
2. Build a static framework from MyStaticLib static library
To build MyStaticLib we will only make use of the terminal. We will create a script that will build a static framework. Run the following command:
cat > build_static_framework.sh <<-EOF
# 0. clean build directoriesrm -rf \
derived_data \
MyStaticLib.framework# 1.1 Build static library for simulatorxcodebuild build \
-scheme MyStaticLib \
-derivedDataPath derived_data \
-arch x86_64 \
-sdk iphonesimulator# 1.2 Build static library for iOS devicesxcodebuild build \
-scheme MyStaticLib \
-derivedDataPath derived_data \
-arch arm64 \
-sdk iphoneos# 2. Create frameworkmkdir MyStaticLib.framework/# 3. Create binary compatible with devices and simulatorslipo -create \
derived_data/Build/Products/Debug-iphoneos/libMyStaticLib.a \
derived_data/Build/Products/Debug-iphonesimulator/libMyStaticLib.a \
-o MyStaticLib.framework/MyStaticLib# 4.1 Create empty public interface directorymkdir MyStaticLib.framework/MyStaticLib.swiftmodule# 4.2 Copy public interfaces for device and simulators into static framework public interfaces directorycp -r derived_data/Build/Products/*/*.swiftmodule/* MyStaticLib.framework/MyStaticLib.swiftmodule
EOF
The above script will:
- Build the static framework twice; once for simulators and once for devices.
- Create a framework folder
- Create a universal binary which is compatible with simulators and devices. This step will use the static libraries built in the first step.
- Create the public interface of the framework. This step will also use the public interfaces built in step 1.
Next let’s run the script. Execute the following command:
sh build_static_framework.sh
A framework, MyStaticLib.framework, will have been created within the same directory.
3. Integrate MyStaticLib static framework into MyApp
For our final step let’s integrate with MyStaticLib.framework
. We’ll first need to link the created framework to MyApp
. Execute the following command:
open . && open -a Xcode ../MyApp/MyApp.xcodeproj
Now MyApp
project is open in Xcode and Finder with MyStaticLib.framework
. Drag and drop MyStaticLib.framework
into the MyApp
project in Xcode.
When prompted to “Choose options for adding these files:” make sure “Copy items if needed” is checked. Select Create Groups for “Added folders option:”. Finally make sure the MyApp target is also checked. Click Finish.
Before we can run our static library code we must make sure that the public interface of MyStaticLib
static framework is accessed during the build time by MyApp
.
Next open MyApp
project in Xcode. Open project configuration settings (the one that says MyApp
with a blue icon to its left on the project navigator). Select MyApp under targets in the editor area. Then select Build Settings tab. Search for Import Paths which lives under Swift Compiler — Search Paths. Change the value to $(PROJECT_DIR)/MyStaticLib.framework
.
Next from menu select Product > Build. 🎉 MyApp
can now successfully consume a static framework. You can now build and run MyApp
with MyStaticLib
for both simulators and devices!
Summary
In this post we have learnt:
- why one should distribute static libraries compiled
- how to build static libraries through Xcode
- how to consume compiled static libraries
- what static frameworks are
- why one should distribute static libraries as static frameworks
- how to build static frameworks
- how to consume static frameworks
Final notes
In this post we have learnt why one should distribute static libraries compiled. We have also learnt how to create static frameworks out of static libraries. These offer better integration experience than compiled static libraries by themselves. However compiled static libraries are more complex to integrate with than compiled dynamic frameworks. Why you ask? There is a lot to cover in that comparison. More on that later.
You can find the full source code to this post here.
Stay tuned for more on iOS development! Follow me on Twitter or Medium.