Modular iOS
Distributing Compiled Swift Frameworks via Cocoapods
This is a followup post to Distributing Swift Frameworks via Cocoapods.
In my previous post, we learnt how to distribute a Swift Framework using Cocoapods. In a nutshell, we achieved this by telling Cocoapods what source code to wrap into a framework. Cocoapods then wrapped the source code and linked it with the integrators project.
We found two potential problems with this solution.
- Source code leaked to the integrator. Allowing them to view and modify our code.
- On every clean build on the integrators app the framework compiled with it. Changes unrelated to the framework required the framework to be recompiled.
So is there a way to solve both of these problems? 🤔 Yes there is! The solution is to compile the framework and then distribute it. But this is easier said than done.
In this post we will cover
- What is a compiled framework.
- Why distribute compiled frameworks.
- Distributing compiled framework
We won’t be covering how to create Swift frameworks. If you want a recap on how to create Swift frameworks checkout my post Reusing code with Swift Framework.
We won’t be delving much into what Cocoapods is and how does it work. Make sure you have Cocoapods installed. You can also checkout my previous post Distributing Swift Frameworks via Cocoapods to learn more about Cocoapods.
Also we won’t cover git in this post. I assume here that you are already familiar with it.
This guide uses Xcode 10.1 and Swift 4.2.
What is a compiled framework?
Swift source is code in a form that iOS developers understand and write. It is in a human readable form. Yet iOS does not understand Swift. It understands binary. So how does iOS run our Swift code?
We translate Swift code to binary. The translation process is referred to as compilation and the final translated form is referred to as compiled code or binary.
iOS can then execute the binary form of the code.
Why distribute compiled code?
In my previous post we distributed the source code of a framework through Cocoapods. Cocoapods then wrapped the source code into a framework on the integrators end. Every time the integrator clean built their app the framework built along with it. That was true even if they did not change the framework source code. Compiling code takes time.
Furthermore the integrator had access to our source code. They were able to view and modify our code.
By distributing compiled code we:
- Keep our code secret
- Save time in the app compilation process
Distributing compiled framework
In this section we will cover the steps to compile an iOS framework and then distribute it through Cocoapods.
Outline:
- Compiling Swift framework via Xcode
- Distributing compiled framework through Cocoapods
- Consuming the compiled framework
- Building and distributing universal iOS frameworks
In the following steps we’ll be using terminal quite frequently to run commands. Make sure it’s handy at all times.
Compiling Swift frameworks via Xcode
Before starting lets fetch a project that contains a framework target. Run the following commands:
cd ~
curl -o MyFramework.zip -L https://www.dropbox.com/s/5vykpag4xb5vh51/MyFramework.zip -s
unzip -q MyFramework.zip
The command above fetches the framework project that we’ll work with in this guide.
The simplest form of compiling a Swift framework is through the Xcode user interface. Let’s open the MyFramework project in Xcode. Run the following commands:
open -a Xcode ~/MyFramework/MyFramework.xcodeproj
Once Xcode is open, select Generic iOS Device
from the device list. Lastly build the framework by selecting from menu Product > Build.
After the build process finishes you’ll find the built framework in the Products
folder (or confusingly named “Groups” in Xcode 🤯) in the project navigator. Right click on MyFramework.framework
and then click on Show in Finder option. The path of the built framework can change and as such let’s use the Show in Finder option to easily locate the built framework. What you’ll see in Finder is a directory named MyFramework.framework
with the compiled MyFramework binary alongside other content required to consume the framework.
Keep the Finder window with our compiled framework handy we will be using it in the next section.
Distributing compiled framework through Cocoapods
In this section we’ll cover how to distribute compiled frameworks through Cocoapods.
We will:
- Create a git repository to hold Cocoapods specifications files in our home directory
- Create a git repository in our home directory to hold the compiled MyFramework
- Create and push MyFramework specification to our Cocoapods specification repository created in step 1
First let’s start by creating a a repository to hold Cocoapods specifications and register that repository with our Cocoapods. Run the following commands:
cd ~
mkdir MySpecs.git
cd MySpecs.git
git init --bare
git clone ~/MySpecs.git ~/MySpecs
cd ~/MySpecs
touch README.md
git add README.md
git commit -m "Initial commit"
git push origin -u master
pod repo add my-specs ~/MySpecs.git
Next, let’s create a repository to store and track MyFramework.framework
.
cd ~
mkdir MyFrameworkDistribution.git
cd MyFrameworkDistribution.git
git init --bare
git clone ~/MyFrameworkDistribution.git ~/MyFrameworkDistribution
cd ~/MyFrameworkDistribution
touch README.md
git add README.md
git commit -m "Initial commit"
git push origin -u master
Now that we have a place to store our built framework let’s drop MyFramework.framework
into the MyFrameworkDistribution
directory. Open MyFrameworkDistribution
directory by running open -a Finder ~/MyFrameworkDistribution
. Now drag and drop MyFramework.framework
into MyFrameworkDistribution
directory.
Once MyFramework.framework
is placed inside MyFrameworkDistribution
repository, we now have to commit the changes to our repository. Let’s tag this snapshot of our repository as version 0.1.0. Run the following commands:
cd ~/MyFrameworkDistribution
git add MyFramework.framework/
git commit -m "Added MyFramework.framework"
git tag -a 0.1.0 -m "Version 0.1.0"
git push origin master
git push origin --tags
The last thing to do is to create a specification for Cocoapods on how to install MyFramework.framework
and share that specification through our Cocoapods specifications repository; we created a repository for that purpose in step 1. Let’s create the specification file by running the following command:
cat > ~/MyFramework/MyFramework.podspec <<-EOFPod::Spec.new do |s|
s.name = "MyFramework"
s.version = "0.1.0"
s.summary = "A brief description of MyFramework project."
s.description = <<-DESC
An extended description of MyFramework project.
DESC
s.homepage = "http://your.homepage/here"
s.license = { :type => 'Copyright', :text => <<-LICENSE
Copyright 2018
Permission is granted to...
LICENSE
}
s.author = { "$(git config user.name)" => "$(git config user.email)" }
s.source = { :git => "$HOME/MyFrameworkDistribution.git", :tag => "#{s.version}" }
s.public_header_files = "MyFramework.framework/Headers/*.h"
s.source_files = "MyFramework.framework/Headers/*.h"
s.vendored_frameworks = "MyFramework.framework"
s.platform = :ios
s.swift_version = "4.2"
s.ios.deployment_target = '12.0'
endEOF
Finally to share our specification let’s run:
pod repo push my-specs ~/MyFramework/MyFramework.podspec
Now we have everything ready to consume our compiled MyFramework
through Cocoapods.
Consuming the compiled framework
In this section we’ll learn how to consume MyFramework.framework
using Cocoapods. First we need an app to consume it. Let’s fetch MyApp project and place it in our home directory. Run the following commands:
cd ~
curl -o MyApp.zip -L https://www.dropbox.com/s/jfdrj58lgrhjc4t/MyApp.zip -s
unzip -q MyApp.zip
Next let’s make MyApp
consume MyFramework.framework
using Cocoapods. To consume MyFramework
we will require to tell Cocoapods that MyApp
requires MyFramework
and where to find the specification of MyFramework
. We do so by creating a Podfile
with all these details and some other about our project structure. Run the following command to create the Podfile
:
cat > ~/MyApp/Podfile <<-EOFtarget 'MyApp' do
use_frameworks!
pod 'MyFramework', '0.1.0', :source => "$HOME/MySpecs.git"
endEOF
Finally run the following commands to tell Cocoapods to fetch MyFramework
and install it into MyApp
:
cd ~/MyApp
pod install
And that's all for distributing and consuming a compiled framework through Cocoapods! Let’s open MyApp.xcworkspace
and then build and run MyApp
inside a simulator.
However if we build the MyApp
for Generic iOS Device from the device list it compiles.
Our compiled framework does not work with iOS simulators. Why? If you recall from Compiling Swift Frameworks via Xcode we selected Generic iOS Device from the device list. This meant that our framework was only being built for devices but not simulators. On the other hand, if we select an iOS simulator then we build only for simulators. If we decide to distribute only device compatible frameworks then integrators of our frameworks won’t be able to test their apps using our framework.
Is there a way we can build MyFramework
in a way that works both for devices and simulators? 🤔 Yes there is! In the next section we’ll cover how.
Building and distributing universal iOS framework
Until now we have been able to build through Xcode and, distribute and consume compiled iOS frameworks through Cocoapods. But we found an issue when consuming our compiled iOS framework that was built via Xcode; we weren’t able to build a framework that is able to work with both simulators and devices through Xcode.
In this section we will look at building our framework that works both with devices and simulators. We will refer to this framework as universal framework. To achieve this we will be building our framework through terminal and using xcodebuild
tool. xcodebuild
is installed alongside Xcode.
We won’t delve too much on how to use xcodebuild
in this post. The important piece to highlight is that we are able to achieve what we did through Xcode user interface using xcodebuild
through terminal. We will now add a script that will:
- build
MyFramework
for simulator - build
MyFramework
for devices - combine the previous two frameworks into a single framework
Run the following command to add the script that will build universal MyFramework
:
cat > ~/MyFramework/build-universal-framework.sh <<-EOF
# create folder where we place built frameworks
mkdir build# build framework for simulators
xcodebuild clean build \
-project MyFramework.xcodeproj \
-scheme MyFramework \
-configuration Release \
-sdk iphonesimulator \
-derivedDataPath derived_data# create folder to store compiled framework for simulator
mkdir build/simulator
# copy compiled framework for simulator into our build folder
cp -r derived_data/Build/Products/Release-iphonesimulator/MyFramework.framework build/simulator#build framework for devices
xcodebuild clean build \
-project MyFramework.xcodeproj \
-scheme MyFramework \
-configuration Release \
-sdk iphoneos \
-derivedDataPath derived_data# create folder to store compiled framework for simulator
mkdir build/devices
# copy compiled framework for simulator into our build folder
cp -r derived_data/Build/Products/Release-iphoneos/MyFramework.framework build/devices
# create folder to store compiled universal framework
mkdir build/universal####################### Create universal framework ############################## copy device framework into universal folder
cp -r build/devices/MyFramework.framework build/universal/# create framework binary compatible with simulators and devices, and replace binary in unviersal framework
lipo -create \
build/simulator/MyFramework.framework/MyFramework \
build/devices/MyFramework.framework/MyFramework \
-output build/universal/MyFramework.framework/MyFramework# copy simulator Swift public interface to universal framework
cp build/simulator/MyFramework.framework/Modules/MyFramework.swiftmodule/* build/universal/MyFramework.framework/Modules/MyFramework.swiftmodule
EOF
Run the following command to execute the script:
cd ~/MyFramework && sh build-universal-framework.sh
The script will output the generated frameworks inside a directory named build
with subdirectories named simulator
, devices
and universal
. In each you’ll find MyFramework.framework
that is compatible with simulators, devices and both respectively.
Let’s share our universal framework through Cocoapods.
Copy the universal framework into the MyFrameworkDistribution
repository. After let’s commit the new framework to our repository. Then let’s tag this version of the framework as 0.1.1. Run the following commands:
cp -r ~/MyFramework/build/universal/MyFramework.framework ~/MyFrameworkDistribution/
cd ~/MyFrameworkDistribution
git add MyFramework.framework
git commit -m "Changed framework from device only compatible to universal"
git push origin master
git tag -a 0.1.1 -m "Version 0.1.1"
git push origin --tags
Next let’s update the MyFramework
specification with out new version (0.1.1). Run the following command:
rm ~/MyFramework/MyFramework.podspec && cat > ~/MyFramework/MyFramework.podspec <<-EOFPod::Spec.new do |s|
s.name = "MyFramework"
s.version = "0.1.1"
s.summary = "A brief description of MyFramework project."
s.description = <<-DESC
An extended description of MyFramework project.
DESC
s.homepage = "http://your.homepage/here"
s.license = { :type => 'Copyright', :text => <<-LICENSE
Copyright 2018
Permission is granted to...
LICENSE
}
s.author = { "$(git config user.name)" => "$(git config user.email)" }
s.source = { :git => "$HOME/MyFrameworkDistribution.git", :tag => "#{s.version}" }
s.source_files = "MyFramework.framework/Headers/*.h"
s.public_header_files = "MyFramework.framework/Headers/*.h"
s.vendored_frameworks = "MyFramework.framework"
s.platform = :ios
s.swift_version = "4.2"
s.ios.deployment_target = '12.0'
endEOF
The final step is to add this new specification of MyFramework
to our specification repository. Run the following command:
pod repo push my-specs ~/MyFramework/MyFramework.podspec
It’s all ready to consume our new version of the framework. Update the Podfile
in MyApp
project to consume our latest version of MyFramework
. Run the following command:
rm ~/MyApp/Podfile && cat > ~/MyApp/Podfile <<-EOFtarget 'MyApp' do
use_frameworks!
pod 'MyFramework', '0.1.1', :source => "$HOME/MySpecs.git"
endEOF
Install the new version of MyFramework
by running cd ~/MyApp && pod install
. Open MyApp.xcworkspace
by running open -a Xcode MyApp.xcworkspace
. Run the MyApp
in a simulator and then in a device.
🎉 We have now successfully deployed a universal framework using Cocoapods.
Final notes
Until recently uploads to the App Store connect (or iTunes Connect) rejected any compiled code for simulators. It looks like that is no longer the case. I was not able to reproduce the error from uploading an app with a framework with compiled code for simulators.
A limitation not covered in this post is that some integrators might not be able to consume your compiled framework if they use another Xcode version. I could write a whole blog post on that. Maybe I will. For now, if you’d like to, you can learn more about it on this reddit discussion.
Distributing compiled frameworks is no easy task. And its not easy to maintain either. So make sure you have well justified the reasons to do so.
Summary
In this post we have learnt:
- What compiled frameworks are and why to use them
- How to build compiled frameworks which are compatible with devices and simulators (universal)
- How to distribute compiled framework through Cocoapods
This is the last part of a series of blog posts on building and distributing Swift frameworks. In my first post I showed how to reuse Swift code using iOS frameworks. Then I followed with a post on how to distribute Swift frameworks using Cocoapods where our Swift code was shared with the integrator. Finally I posted on to how share Swift frameworks through Cocoapods without sharing our Swift code.
Until next time …
I love and prefer Swift over other languages supported by iOS. However in some cases I can’t use Swift in iOS. One such case is when we want to consume a C++ library.
At Onfido we use OpenCV to process images from the iOS device camera to ensure we capture the most readable and highest quality capture of documents; OpenCV is written in C++.
In the next blog post I will show you how to process images from the camera on iPhones. We will process the images using OpenCV and then display results back to the user. We will learn how to use other languages on the iOS platform to make it happen. In the blog post following that I will show you how to reuse Swift frameworks that consumes a OpenCV.