Swift packages 是可重複使用的程式碼元件。它不但可以包含程式碼,還可以包含二進位 XCFrameworks。發佈 XCFrameworks 可以保護程式碼不外洩。本文章將介紹如何使用 Swift packages 發佈 XCFrameworks。
Table of Contents
建立 XCFrameworks
如果你不熟悉如何建立 XCFrameworks,請先參照以下文章。
首先我們建立一個 framework 叫 GreetingUI,並在裡面宣告 GreetingViewController,其程式碼如下。
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!" } }
接下來用下面的 shell script 產生 XCFramework。它會在 archives 資料夾下產生GreetingUI.xcframework。
#!/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"
用 Swift Packages 發佈 XCFrameworks
如果你不熟悉如何建立 Swift packages,請先參照以下文章。
Swift packages 支援兩種方式來發佈 XCFrameworks。一種是將 XCFrameworks 和 Swift packages 放在同一個硬碟。另一種是將 XCFrameworks 與 Swift packages 放在不同的主機。
Swift Packages 指向在同一個硬碟的 XCFrameworks
建立一個 Swift package 叫 GreetingUISDK。將剛剛產生出來的 GreetingUI.xcframework 移至 GreetingUISDK 資料夾下。
在 Package.swift 中,用 .binaryTarget()
來新增一個 target,其 name
為 GreetingUI 和 path
為 GreetingUI.xcframework。然後,將 target GreetingUI 加入到 product GreetingUISDK 的 targets
裡面。
如果 GreetingUISDK 沒有包含任何程式碼的話,那我們可以移除 target GreetingSDK,且也要將它從 product GreetingUISDK 的 targets
中移除。
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"]), ] )
最後用 Git tag 建立版本 1.0.0,然後將 GreetingUISDK push 到 https://github.com/xhhuango/GreetingUISDK 後,就發佈完成了。
Swift Packages 指向 Remote XCFrameworks
我們也可以將 Swift Package 和 XCFramework 放在不同的主機。
用 zip 將 GreetingUI.xcframework 打包成 GreetingUI.xcframework.zip,再用以下指令計算 GreetingUI.xcframework.zip 的 checksum。將 GreetingUI.xcframework.zip 放到某個主機上。
% swift package compute-checksum GreetingUI.xcframework.zip 758990849603c7be90c8ff277cd0a004e649770773492cf9f526a4baad5192f9
在 Package.swift 中,用 .binaryTarget() 來新增一個 target,並且指定其 name
、 GreetingUI.xcframework.zip 的 url
、和剛剛計算的 checksum
。
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"]), ] )
最後用 Git tag 建立版本 1.1.0,然後將 GreetingUISDK push 到 https://github.com/xhhuango/GreetingUISDK 後,就發佈完成了。
加入包含 XCFrameworks 的 Swift Packages 依賴到 Apps
如果你不熟悉如何加入 Swift package 依賴到 Apps,請先參照以下文章。
建立 GreetingApp 專案,用 https://github.com/xhhuango/GreetingUISDK 加入 GreetingUISDK 依賴。
我們現在可以在 GreetingApp 中使用 GreetingUI.GreetingViewController,如下。
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() } }
用 Swift Packages 發佈有依賴其他 Swift Packages 的 XCFrameworks
如果 XCFramework 有依賴其他 local 或 open-source Swift packages,那當你使用以上所介紹的方式的話,在編譯 App 時會發生錯誤。
假設 GreetingUI 有依賴 Toast-Swift,並且在 GreetingViewController 裡面使用 Toast-Swift,如下程式碼。
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!" } }
然後,我們產生 GreetingUI.xcframework,並用之前介紹的方式發佈到 https://github.com/xhhuango/GreetingUISDK,且建立版本 2.0.0。
在 GreetingApp 中,加入版本 2.0.0 的 GreetingUISDK 依賴,編譯時 Xcode 會顯示 No such module Toast 的錯誤。
這是因為在 GreetingUI.xcframework/ios-arm64_x86_64-simulator/GreetingUI.framework/Modules/GreetingUI.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface 裡面有一行 import Toast。
// 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 }
當在產生 GreetingUI.xcframework 時,xcodebuild 編譯 Toast-Swift,並 statically link Toast-Swift 至 GreetingUI.xcframework,所以沒有產生 Toast-Swift 的 .swiftmodule。因此 GreetingApp 無法找到 Toast 模組。
在 Issue with third party dependencies inside a XCFramework through SPM 中,NeoNacho 提到兩個解決方案。
第一個方案是,在產生 GreetingUI.xcframework 時,要求 xcodebuild 不在要再 GreetingUI.xcframework/ios-arm64_x86_64-simulator/GreetingUI.framework/Modules/GreetingUI.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface 裡加上 import Toast。我們可以在 GreetingUI 裡使用 @_implementationOnly import 來 privately import Toast,如下。
但這會產生一個問題。如果使用 @_implementationOnly import Toast 的話,你就不可以在 GreetingUI 的 public interface 中使用到 Toast 的 symbols。
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!" } }
第二個方案是,將 Toast-Swift 編譯成 Toast.xcframework,然後 GreetingUI 依賴 Toast.xcframework。這樣 GreetingUI.xcframework 就不會 statically link Toast-Swift。
但這樣做的話,在發佈 GreetingUISDK 時,GreetingUISDK 必須要包含 GreetingUI.xcframework 和 Toast.xcframework。如果 GreetingUI 依賴多個 Swift packages 的話,就必須要將這些 Swift packages 一一編譯成 xcframework。發佈流程會變得複雜。
你需要將 Toast.xcframework 也移到 GreetingUISDK 資料夾下,然後修改 Package.swift 如下。
// 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"]), ] )
結語
當我們發佈的產品是一個二進位 xcframework 時,我們會需要保護程式碼不外洩。目前 Swift packages 可以支援發佈二進位 xcframework。但當我們的 xcframework 有依賴其他的 Swift packages,會有 symbol references 的問題。希望未來 Xcode 會改善這部分。