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 .
Table of Contents
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.
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.
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 SPM, NeoNacho 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.