Modular iOS

How to distribute compiled iOS frameworks using Swift Package Manager

There is a new way to distribute binaries. Fully supported by Apple!

Anurag Ajwani

--

Photo by RoseBox رز باکس on Unsplash

Do you want to distribute your iOS framework to other developers compiled? You may want to hide you source code. Maybe you want to save integrators from having to compile it.

The release of Xcode 12 beta adds a new way for you to distribute your compiled iOS framework — that is using Swift Package Manager. However Swift Package Manager does not directly support distribution of frameworks themselves but rather frameworks wrapped inside of an XCFramework.

In this post I won’t go much in detail of what XCFrameworks are and how to build them. In this post I’ll cover how to distribute these and consume them using Swift Package Manager.

I assume you are already familiar with iOS frameworks, how to build these and XCFrameworks. Furthermore you must be familiar with iOS development, git version control and Github. Another pre-requisite is to have your Github account connected to Xcode.

Note Xcode 12.0 beta is required for this post.

Publishing compiled iOS frameworks for Swift Package Manager

In this section we’ll start by downloading an existing iOS framework project. Next we’ll build the framework and wrap it in an XCFramework. Then we’ll create a package specification. Finally we’ll publish the package specification and XCFramework to Github.

The steps we’ll take:

  1. Retrieve the starter pack
  2. Building the XCFramework
  3. Create git repository to host the XCFramework
  4. Add the XCFramework to git repository
  5. Create Package specification
  6. Publish package specification to the git repository
  7. Push copy of the git repository to Github

1. Retrieve the starter pack

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

cd $HOME
curl https://github.com/anuragajwani/swiftpm-xcframework-distribution/archive/starter.zip -o swiftpm-xcframework-tut-starter.zip -L -s
unzip -q swiftpm-xcframework-tut-starter.zip
cd swiftpm-xcframework-distribution-starter/MyFramework

The starter pack contains a iOS framework project and an iOS 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 framework when the user taps the login button.

2. Building the XCFramework

The starter pack’s framework project also includes a script to build an XCFramework. Knowledge of how to build XCFrameworks from frameworks is not required for this post.

Let’s build the XCFramework. Execute the following command:

sh build_xcframework.sh

The script above builds the framework for iOS devices and simulators and then wrap these into an XCFramework. Upon successful execution the script will output a file named MyFramework.xcframework inside the build directory where the script is located.

3. Create git repository to host the XCFramework

Next we’ll create a git repository to host the XCFramework. In this tutorial we’re using git to version and track our XCFramework releases. Execute the following commands:

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

4. Add the XCFramework to git repository

Next we’ll add the XCFramework to the local git repository. Execute the following commands:

cp -r ~/swiftpm-xcframework-distribution-starter/MyFramework/MyFramework.xcframework ~/MyFrameworkDistribution/
cd ~/MyFrameworkDistribution
git add --all
git commit -m "Added XCFramework"

5. Create Package specification

Next we’ll create the Package specification inside the git repository. The package specification is a file that lets Swift Package Manager know how to consume the package — in this case the XCFramework. Execute the following command:

cat > ~/MyFrameworkDistribution/Package.swift <<-EOF
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "MyFramework",
platforms: [
.iOS(.v11)
],
products: [
.library(
name: "MyFramework",
targets: ["MyFramework"])
],
targets: [
.binaryTarget(
name: "MyFramework",
path: "MyFramework.xcframework")
])
EOF

The most important bit to note for this post is:

.binaryTarget(
name: "MyFramework",
path: "MyFramework.xcframework")

This was specifically added in Swift 5.3 and is the one that allows publishing and consuming compiled source using Swift Package Manager. Here we are saying that the target is compiled, the name of the target and where it lives within our repository.

If you’d like to learn more about rest of Package.swift you can find more about it in my previous post or in Apple’s official documentation site.

6. Publish package specification to the git repository

Next we need to publish the package specification to the local git repository. Execute the following commands:

cd ~/MyFrameworkDistribution
git add Package.swift
git commit -m "Added package specification"

Finally we need to tag this release of our XCFramework. The tag is how Swift Package Manager on the integrators app will find the version the integrator desires to install. Let’s say the integrator wishes to install version 1.1.0 of you XCFramework. Swift Package Manager will look through the tags and use version that matches the tag 1.1.0.

For this first release of our XCFramework we’ll tag it 1.0.0. Execute the following command:

git tag -a 1.0.0 -m "Version 1.0.0"

7. Push copy of the git repository to Github

Next we’ll push a copy of our git repository to Github. For such we’ll need to create a new repository on Github. Click on the link below:

Next name the repository as MyFrameworkDistribution. You can make your repo private if you like. Click on Create Repository.

Next let’s push a copy of our local git repository to the Github repository just created. Back in Terminal execute the following commands (remember to replace <YOUR_GITHUB_USERNAME> with your Github username):

cd ~/MyFrameworkDistribution
git remote add github git@github.com:<YOUR_GITHUB_USERNAME>/MyFrameworkDistribution.git
git push github -u master
git push github --tags

And that is it. Our XCFramework is now published and ready to be consumed by Swift Package Manager!

Consuming XCFramework using Swift Package Manager

In this section we’ll consume the XCFramework we published in the previous section. We’ll be using an iOS app project already included within the starter pack we downloaded in the previous section.

The steps we’ll take:

  1. Import XCFramework to the app
  2. Consume the XCFramework code

1. Import XCFramework to the app

First let’s open the app included in the starter pack. Execute the following command:

open -a Xcode-beta ~/swiftpm-xcframework-distribution-starter/SwiftPMXCFrameworkDemo/SwiftPMXCFrameworkDemo.xcodeproj

Note I have saved my copy of Xcode 12.0 beta in my Applications folder.

Next let’s import the XCFramework. From menu select File > Swift Packages > Add Package Dependency…

Next when prompted “Choose Package Repository” search and select MyFrameworkDistribution. Click Next.

When prompted to “Choose Package Options” leave the options default. XCode already fills out the version to 1.0.0 automatically. Click Next.

Finally when prompted to “Add Package to SwiftPMXCFrameworkDemo:” make sure MyFramework Package is selected and the target destination (under “Add to Target” column) is SwiftPMXCFrameworkDemo. Click Finish.

2. Consume the XCFramework code

For the last step of this tutorial we’ll consume the code contained within the XCFramework. Within Xcode open ViewController.swift and under import UIKit add the following line of code:

import MyFramework

Next within the loginButtonTapped function add the following lines of code:

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

And that is it. Run the code in any simulator or device and see it work! 🎉

Summary

In this post we learn’t:

  • how to distribute compiled frameworks (as XCFrameworks) through Swift Package Manager
  • how to consume compiled frameworks (as XCFrameworks) through Swift Package Manager

Final Notes

For a long time your only option to distribute compiled iOS frameworks was by using an third-party dependency managers such as Cocoapods or Carthage. Furthermore building universal iOS frameworks — one which works with simulators and devices in one artefact — was not supported by Xcode out of the box.

Since Xcode 11.0 Apple gave us the ability to distribute universally compatible frameworks through XCFrameworks. With Xcode 12.0 the Swift community and Apple have brought us the capability of distributing XCFrameworks through SwiftPM.

I have been maintaining and distributing a compiled iOS framework for nearly 4 years now. Until now I have felt that many of the solutions I applied to distribute the compiled framework was hacky and brittle. Every WWDC I wondered what would break next. However its now possible that these will experiences of the past! WWDC 2020 is possibly my favourite WWDC ever ♥️

--

--

Anurag Ajwani

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