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.

  1. Source code leaked to the integrator. Allowing them to view and modify our code.
  2. 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.

Select Generic iOS Device from the device list
Building MyFramework

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.

Locate MyFramework.framework
Compiled MyFramework in Finder

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:

  1. Create a git repository to hold Cocoapods specifications files in our home directory
  2. Create a git repository in our home directory to hold the compiled MyFramework
  3. 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.

Add MyFramework.framework to git repository

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 <<-EOF
Pod::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'
end
EOF

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 <<-EOF
target 'MyApp' do
use_frameworks!
pod 'MyFramework', '0.1.0', :source => "$HOME/MySpecs.git"
end
EOF

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.

MyApp does not build successfully for simulators

However if we build the MyApp for Generic iOS Device from the device list it compiles.

MyApp builds successfully for devices

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 <<-EOF
Pod::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'
end
EOF

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 <<-EOF
target 'MyApp' do
use_frameworks!
pod 'MyFramework', '0.1.1', :source => "$HOME/MySpecs.git"
end
EOF

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.

MyApp successfully builds consuming MyFramework in simulators and devices

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

Stay tuned by following me on Twitter or Medium!