Skip to content

Swift packages with binary targets

Chris Davis·iOS Developer

Published on

Swift packages are a wonderful way of distributing code to use in projects. They are simple to use, you add the URL to a repo in Xcode and the dependency is added to your project. Almost like magic.

Casual users will deploy their Swift package as pure Swift code, however this has some drawbacks (if you see them as drawbacks)

  • Your code is public, without correct licensing it could be used and abused

What if there was a way to offer a Swift package to your consumers without having to share your source code?

We'll cover:

Swift package binary targets

They were built with this in mind.

In this tutorial I'll show you how to create a Swift package that can be exported as an XCFramework which is used in a distributed Swift package.

At the end you will have:

  • A private Swift package with your original source
  • A public Swift package that contains an XCFramework which you can distribute freely

Creating your private Swift package

Create a new Swift package.

Xcode Vision App

I've called my package Bookshop.

You'll see the Package.swift, I've set the supported platforms to be visionOS.

// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "Bookshop",
    platforms: [.visionOS(.v1)],
    products: [
        // Products define the executables and libraries a package produces, making them visible to other packages.
        .library(
            name: "Bookshop",
            targets: ["Bookshop"]),
    ],
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .target(
            name: "Bookshop"),
        .testTarget(
            name: "BookshopTests",
            dependencies: ["Bookshop"]),
    ]
)

I'm going to add a single method to the Bookshop.swift file so that we have something to use.

public class Bookshop {
  public func buy(book: String) {
    print("Book: \(book) purchased")
  }
}

Note I've made the class and method public so that we can use them from other code.

Creating an XCFramework

We're now going to create an *.xcframework file that consumers can use in their app

Platforms

You'll note that when using Xcode, you can use the Simulator, or run on Device, classically these devices have different architectures, ie, x86, arm64. This means that we are going to need to create an XCFramework with multiples slices per architecture. If we didn't do this, the package wouldn't run.

Script

We need to build the physical device slice

SCHEME="Bookshop"
xcodebuild archive -workspace . -scheme $SCHEME -destination "generic/platform=visionOS" -archivePath /tmp/xcf/xros.xcarchive -derivedDataPath /tmp/visionos -sdk xros SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES

Then create the build for the simulator slice

SCHEME="Bookshop"
xcodebuild archive -workspace . -scheme $SCHEME -destination "generic/platform=visionOS Simulator" -archivePath /tmp/xcf/xrsimulator.xcarchive -derivedDataPath /tmp/visionos -sdk xrsimulator SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES

Merging

We have both frameworks, however, how do we create a single XCFramework from them?

xcodebuild -create-xcframework -framework $(readlink -f "/tmp/xcf/xros.xcarchive/Products/usr/local/lib/Bookshop.framework") -framework $(readlink -f "/tmp/xcf/xrsimulator.xcarchive/Products/usr/local/lib/Bookshop.framework") -output "/tmp/xcf/Bookshop.xcframework"

Static vs dynamic

If you run the last script, it will fail...this is because as it stands, the Swift package produces a static framework, and the output is an object file, not a framework.

We can fix this by modifying the Package.swift to:

// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "Bookshop",
    platforms: [.visionOS(.v1)],
    products: [
        // Products define the executables and libraries a package produces, making them visible to other packages.
        .library(
            name: "Bookshop",
            type: .dynamic,
            targets: ["Bookshop"]),
    ],
    targets: [
        // Targets are the basic building blocks of a package, defining a module or a test suite.
        // Targets can depend on other targets in this package and products from dependencies.
        .target(
            name: "Bookshop"),
        .testTarget(
            name: "BookshopTests",
            dependencies: ["Bookshop"]),
    ]
)

Run the scripts again.

Creating your public Swift package

Create a new Swift package and copy the generated xcframework into the Sources folder.

Update your new Package.swift to reference the xcframework.

// swift-tools-version: 5.9
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
    name: "Bookshop",
    platforms: [.visionOS(.v1)],
    products: [
        // Products define the executables and libraries a package produces, making them visible to other packages.
        .library(
            name: "Bookshop",
            targets: ["BookshopFramework"]),
    ],
    targets: [
        .binaryTarget(name: "BookshopFramework",
                      path: "assets/Sources/Bookshop.xcframework")
    ]
)

Deploy

Deploy this package, your folder structure should be succinct to GitHub, ie, you should not see any of your public code.

Package contents

Importing

Inside of Xcode import the package, use the GitHub url.

Fail to import

You can now use the code in your app...or can you?

import Bookshop

public class MyApp {

  func init() {
    let bookshop = Bookshop()
    bookshop.buy(book: "some_book")
  }

}

If you try to compile, you get No such module Bookshop...why is that?

Modules

We have an XCFramework but to Xcode it's a bunch of data, like being on an island without a map, we need a map, specifically a modulemap

Let's modify the scripts so that we can include the generated swiftmodule in our framework.

We need to build the physical device slice

SCHEME="Bookshop"
xcodebuild archive -workspace . -scheme $SCHEME -destination "generic/platform=visionOS" -archivePath /tmp/xcf/xros.xcarchive -derivedDataPath /tmp/visionos -sdk xros SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES
mkdir /tmp/xcf/xros.xcarchive/Products/usr/local/lib/Bookshop.framework/Modules
cp -a /tmp/visionos/Build/Intermediates.noindex/ArchiveIntermediates/Bookshop/BuildProductsPath/Release-xros/Bookshop.swiftmodule /tmp/xcf/xros.xcarchive/Products/usr/local/lib/Bookshop.framework/Modules

Then create the build for the simulator slice

SCHEME="Bookshop"
xcodebuild archive -workspace . -scheme $SCHEME -destination "generic/platform=visionOS Simulator" -archivePath /tmp/xcf/xrsimulator.xcarchive -derivedDataPath /tmp/visionos -sdk xrsimulator SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES
mkdir /tmp/xcf/xrsimulator.xcarchive/Products/usr/local/lib/Bookshop.framework/Modules
cp -a /tmp/visionos/Build/Intermediates.noindex/ArchiveIntermediates/Bookshop/BuildProductsPath/Release-xrsimulator/Bookshop.swiftmodule /tmp/xcf/xrsimulator.xcarchive/Products/usr/local/lib/Bookshop.framework/Modules

Recombine the two frameworks to make a single XCFramework

xcodebuild -create-xcframework -framework $(readlink -f "/tmp/xcf/xros.xcarchive/Products/usr/local/lib/Bookshop.framework") -framework $(readlink -f "/tmp/xcf/xrsimulator.xcarchive/Products/usr/local/lib/Bookshop.framework") -output "/tmp/xcf/Bookshop.xcframework"

Re-run the script and re-upload your Package to GitHub.

When you now import, you can buy your book. Magic.

Useful commands

You may not know what Xcode version, or SDKs you have installed on your system, these one-liners can help you out:

  • What version of Xcode am I using?
xcode-select -p
  • What destinations are available for the scheme?
xcodebuild -showdestinations -scheme Bookshop
  • What SDKs do I have installed?
xcodebuild -showsdks

At ustwo, we take pride in the craft of our Swift iOS development. If you'd like to have a chat about working with ustwo or joining our team, head over to ustwo.com