Distributing XCFrameworks as Swift Packages

Photo by Ozgu Ozden on Unsplash
Photo by Ozgu Ozden on Unsplash
Swift packages are reusable code components. It can contain not only code, but also binary XCFrameworks. Distributing XCFrameworks can protect source code.

Swift packages are reusable code components. It can contain not only code, but also binary XCFrameworks. Distributing XCFrameworks can protect source code. This article will explain how to distribute XCFrameworks as Swift packages.

The complete code for this chapter can be found in .

Creating XCFrameworks

If you are not familiar with how to create XCFrameworks, please refer to the following article first.

First, we create a framework called GreetingUI, and declare GreetingViewController as below.

import UIKit

public class GreetingViewController: UIViewController {
    public static func newInstance() -> GreetingViewController {
        let storyboard = UIStoryboard(name: "Greeting", bundle: Bundle(for: Self.self))
        let viewController = storyboard.instantiateViewController(withIdentifier: "GreetingViewController") as! GreetingViewController
        return viewController
    }
    
    @IBOutlet weak var greetingLabel: UILabel!
    
    public override func viewDidLoad() {
        super.viewDidLoad()

        greetingLabel.text = "Hello Wayne's Talk!"
    }
}

Next use the following shell script to create a XCFramework. It will create GreetingUI.xcframework under the archives folder.

#!/bin/sh

PROJECT_NAME="GreetingUI"
OUTPUT_DIR="archives"

xcodebuild archive \
  -project "$PROJECT_NAME/$PROJECT_NAME.xcodeproj" \
  -scheme "$PROJECT_NAME" \
  -destination "generic/platform=iOS" \
  -archivePath "archives/iOS" \
  SKIP_INSTALL=NO \
  BUILD_LIBRARY_FOR_DISTRIBUTION=YES

xcodebuild archive \
  -project "$PROJECT_NAME/$PROJECT_NAME.xcodeproj" \
  -scheme "$PROJECT_NAME" \
  -destination "generic/platform=iOS Simulator" \
  -archivePath "archives/iOS-Simulator" \
  SKIP_INSTALL=NO \
  BUILD_LIBRARY_FOR_DISTRIBUTION=YES

xcodebuild -create-xcframework \
    -framework "$OUTPUT_DIR/iOS.xcarchive/Products/Library/Frameworks/$PROJECT_NAME.framework" \
    -framework "$OUTPUT_DIR/iOS-Simulator.xcarchive/Products/Library/Frameworks/$PROJECT_NAME.framework" \
    -output "$OUTPUT_DIR/$PROJECT_NAME.xcframework"

Distributing XCFrameworks as Swift Packages

If you are not familiar with how to create Swift packages, please refer to the following articles first.

Swift packages support two ways to distribute XCFrameworks. One is to place XCFrameworks and Swift packages on the same disk. The other is to place XCFrameworks and Swift packages on different hosts.

Swift Packages Referring to XCFrameworks on the same disk

Create a Swift package called GreetingUISDK. Move the newly created GreetingUI.xcframework to the GreetingUISDK folder.

In Package.swift, use .binaryTarget() to add a new target with name GreetingUI and path GreetingUI.xcframework. Then, add target GreetingUI to the targets of product GreetingUISDK.

If GreetingUISDK does not contain any code, then we can remove the target GreetingSDK, and also remove it from the targets of product GreetingUISDK.

import PackageDescription

let package = Package(
    name: "GreetingUISDK",
    products: [
        // Products define the executables and libraries a package produces, and make them visible to other packages.
        .library(name: "GreetingUISDK", targets: ["GreetingUISDK", "GreetingUI"]),
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(name: "GreetingUISDK", dependencies: []),
        .binaryTarget(name: "GreetingUI", path: "GreetingUI.xcframework"),
        .testTarget(name: "GreetingUISDKTests", dependencies: ["GreetingUISDK"]),
    ]
)

Finally, create version 1.0.0 with Git tag, and then push GreetingUISDK to https://github.com/xhhuango/GreetingUISDK, the release process is complete.

Swift Packages Referring to Remote XCFrameworks

We can also place Swift Package and XCFramework on different hosts.

Zip GreetingUI.xcframework to GreetingUI.xcframework.zip, and then use the following command to compute the checksum of GreetingUI.xcframework.zip. Place GreetingUI.xcframework.zip on a host.

% swift package compute-checksum GreetingUI.xcframework.zip
758990849603c7be90c8ff277cd0a004e649770773492cf9f526a4baad5192f9

In Package.swift, use .binaryTarget() to add a new target, and specify its name, the url of GreetingUI.xcframework.zip, and the checksum just calculated.

import PackageDescription

let package = Package(
    name: "GreetingUISDK",
    platforms: [.iOS(.v13)],
    products: [
        // Products define the executables and libraries a package produces, and make them visible to other packages.
        .library(name: "GreetingUISDK", targets: ["GreetingUISDK", "GreetingUI"]),
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(name: "GreetingUISDK", dependencies: []),
        .binaryTarget(
            name: "GreetingUI", 
            url: "https://your.domain.com/path/GreetingUI.xcframework.zip",
            checksum: "758990849603c7be90c8ff277cd0a004e649770773492cf9f526a4baad5192f9"),
        .testTarget(name: "GreetingUISDKTests", dependencies: ["GreetingUISDK"]),
    ]
)

Finally, create version 1.1.0 with Git tag, and then push GreetingUISDK to https://github.com/xhhuango/GreetingUISDK, the release process is complete.

Add Swift Packages that include XCFrameworks as a dependency to Apps

If you are not familiar with how to add Swift package dependencies to Apps, please refer to the following article first.

Create GreetingApp project and use https://github.com/xhhuango/GreetingUISDK to add GreetingUISDK dependency.

GreetingApp has GreetingUISDK dependency.
GreetingApp has GreetingUISDK dependency.

We can now use GreetingUI.GreetingViewController in GreetingApp as follows.

import UIKit
import GreetingUI

class ViewController: UIViewController {
    @IBAction func onClick(_ sender: Any) {
        let viewController = GreetingViewController.newInstance()
        present(viewController, animated: true)
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}

Distributing XCFrameworks depending on other Swift Packages as Swift Packages

If XCFramework depends on other local or open-source Swift packages, then when you use the way described above, an error will occur when compiling the App.

Suppose GreetingUI depends on Toast-Swift, and Toast-Swift is used in GreetingViewController, as shown in the following code.

import UIKit
import Toast

public class GreetingViewController: UIViewController {
    public static func newInstance() -> GreetingViewController {
        let storyboard = UIStoryboard(name: "Greeting", bundle: Bundle(for: Self.self))
        let viewController = storyboard.instantiateViewController(withIdentifier: "GreetingViewController") as! GreetingViewController
        return viewController
    }
    
    @IBOutlet weak var greetingLabel: UILabel!
    
    @IBAction func onClick(_ sender: Any) {
        view.makeToast("Hello Wayne's Talk!")
    }
    
    public override func viewDidLoad() {
        super.viewDidLoad()

        greetingLabel.text = "Hello Wayne's Talk!"
    }
}

Then, we generate GreetingUI.xcframework and publishe it to https://github.com/xhhuango/GreetingUISDK in the way introduced earlier, and create version 2.0.0.

In GreetingApp, add the version 2.0.0 of GreetingUISDK dependency, and Xcode will display an error of No such module Toast when compiling.

Xcode complains "No such module Toast".
Xcode complains “No such module Toast”.

This is because there is a line containing import Toast in GreetingUI.xcframework/ios-arm64_x86_64-simulator/GreetingUI.framework/Modules/GreetingUI.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface.

// swift-interface-format-version: 1.0
// swift-compiler-version: Apple Swift version 5.7.2 (swiftlang-5.7.2.135.5 clang-1400.0.29.51)
// swift-module-flags: -target x86_64-apple-ios16.2-simulator -enable-objc-interop -enable-library-evolution -swift-version 5 -enforce-exclusivity=checked -O -module-name GreetingUI
// swift-module-flags-ignorable: -enable-bare-slash-regex
@_exported import GreetingUI
import Swift
import Toast
import UIKit
import _Concurrency
import _StringProcessing
@objc @_inheritsConvenienceInitializers @_Concurrency.MainActor(unsafe) public class GreetingViewController : UIKit.UIViewController {
  @_Concurrency.MainActor(unsafe) public static func newInstance() -> GreetingUI.GreetingViewController
  @_Concurrency.MainActor(unsafe) @objc override dynamic public func viewDidLoad()
  @_Concurrency.MainActor(unsafe) @objc override dynamic public init(nibName nibNameOrNil: Swift.String?, bundle nibBundleOrNil: Foundation.Bundle?)
  @_Concurrency.MainActor(unsafe) @objc required dynamic public init?(coder: Foundation.NSCoder)
  @objc deinit
}

When generating GreetingUI.xcframework, xcodebuild compiles Toast-Swift and statically links Toast-Swift to GreetingUI.xcframework, so Toast-Swift’s .swiftmodule is not generated. As the result, GreetingApp cannot find the Toast module.

In Issue with third party dependencies inside a XCFramework through SPMNeoNacho mentions two solutions.

The first solution is to ask xcodebuild not to add import Toast in GreetingUI.xcframework/ios-arm64_x86_64-simulator/GreetingUI.framework/Modules/GreetingUI.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface when generating GreetingUI.xcframewor We can use @_implementationOnly import in GreetingUI to privately import Toast, as follows.

But this creates a problem. If you use @_implementationOnly import Toast, you cannot use Toast symbols in GreetingUI’s public interface.

import UIKit
@_implementationOnly import Toast

public class GreetingViewController: UIViewController {
    public static func newInstance() -> GreetingViewController {
        let storyboard = UIStoryboard(name: "Greeting", bundle: Bundle(for: Self.self))
        let viewController = storyboard.instantiateViewController(withIdentifier: "GreetingViewController") as! GreetingViewController
        return viewController
    }
    
    @IBOutlet weak var greetingLabel: UILabel!
    
    @IBAction func onClick(_ sender: Any) {
        view.makeToast("Hello Wayne's Talk!")
    }
    
    public override func viewDidLoad() {
        super.viewDidLoad()

        greetingLabel.text = "Hello Wayne's Talk!"
    }
}

The second solution is to compile Toast-Swift into Toast.xcframework, and then GreetingUI depends on Toast.xcframework. This way GreetingUI.xcframework does not statically link Toast-Swift.

But in doing so, GreetingUISDK must contain GreetingUI.xcframework and Toast.xcframework when GreetingUISDK is released. If GreetingUI depends on multiple Swift packages, these Swift packages must be built into xcframework one by one. The publishing process can get complicated.

You need to move Toast.xcframework to the GreetingUISDK folder, and then modify Package.swift as follows.

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

import PackageDescription

let package = Package(
    name: "GreetingUISDK",
    platforms: [.iOS(.v13)],
    products: [
        // Products define the executables and libraries a package produces, and make them visible to other packages.
        .library( name: "GreetingUISDK", targets: ["GreetingUISDK", "GreetingUI", "Toast"]),
    ],
    dependencies: [
        // Dependencies declare other packages that this package depends on.
        // .package(url: /* package url */, from: "1.0.0"),
    ],
    targets: [
        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
        // Targets can depend on other targets in this package, and on products in packages this package depends on.
        .target(name: "GreetingUISDK", dependencies: []),
        .binaryTarget(name: "GreetingUI", path: "GreetingUI.xcframework"),
        .binaryTarget(name: "Toast", path: "Toast.xcframework"),
        .testTarget(name: "GreetingUISDKTests", dependencies: ["GreetingUISDK"]),
    ]
)

Conclusion

When our product is a binary xcframework, we will need to protect the source code. Currently Swift packages can support distributing binary xcframework. But when our xcframework depends on other Swift packages, there will be problems with symbol references. Hopefully Xcode will improve this part in the future.

Leave a Reply

Your email address will not be published. Required fields are marked *

You May Also Like
Photo by Alex Alvarez on Unsplash
Read More

Dispatch Queue Tutorial

Grand Central Dispatch (GCD) provides efficient concurrent processing so that we don’t need to directly manage multiple threads. Its dispatch queues can execute tasks serially or concurrently.
Read More