XCFramework和Swift Package實用學習筆記

2021/07/31

XCFramework和Swift Package實用學習筆記

一點背景

一步步創建XCFramework

我们试着做一个跨平台的框架,代码本身并不重要,主要是为了展示建立XCFramework的过程。打开Xcode,我們先來建立一個Framework项目:

創建框架

我們的代碼擴展UIColor,提供一些類似膠片和古畫裡,有點褪色的色彩定義,鑑於此,我們起名為PaleInk。建立一個iOS的framework,PaleInk-iOS,並增加一个Swift文件UIColor+Ink.swift。先給UIColor加兩個convenient initialiser,可以用RGB值以及繪圖軟件常用的16進制值來創建UIColor實例:

import UIKit

public extension UIColor {

	// Create a UIColor from RGB
    convenience init(red: Int, green: Int, blue: Int, a: CGFloat = 1.0) {
        self.init(
            red: CGFloat(red) / 255.0,
            green: CGFloat(green) / 255.0,
            blue: CGFloat(blue) / 255.0,
            alpha: a
        )
    }

    // Create a UIColor from a hex value (E.g 0x000000)
    convenience init(hex: Int, a: CGFloat = 1.0) {
        self.init(
            red: (hex >> 16) & 0xFF,
            green: (hex >> 8) & 0xFF,
            blue: hex & 0xFF,
            a: a
        )
    }
}

同樣的步驟,創建一個macOS的framework,增加同樣的代碼:

import AppKit

public extension NSColor {

    convenience init(red: Int, green: Int, blue: Int, a: CGFloat = 1.0) {
        self.init(
            calibratedRed: CGFloat(red) / 255.0,
            green: CGFloat(green) / 255.0,
            blue: CGFloat(blue) / 255.0,
            alpha: a
        )
    }
    
    // Create a UIColor from a hex value (E.g 0x000000)
    convenience init(hex: Int, a: CGFloat = 1.0) {
        self.init(
            red: (hex >> 16) & 0xFF,
            green: (hex >> 8) & 0xFF,
            blue: hex & 0xFF,
            a: a
        )
    }
}

我們把兩個不同平台的Framework放在同一個目錄裡:

兩個框架放在一個目錄裡

接下來我們來打包這兩個Framework(archive),保存默認Archive的目錄是/Users/username/Library/Developer/Xcode/Archives,也可以自己定製。我們自己在上圖同樣的目錄下建立一個新的Archive目录。

打包Archive可以用XCode完成,由于工作中许多事情是CI用命令行来做,我們這裡也採用命令行的方式。先來到PaleInk-iOS目錄:

hzxu$ pwd
/Users/hzxu/Projects/PaleInk/PaleInk-iOS

然後執行命令:

hzxu$ xcodebuild archive \
> -scheme PaleInk-iOS \
> -destination "generic/platform=iOS" \
> -archivePath ../Archive/PaleInk-iOS \
> SKIP_INSTALL=NO \
> BUILD_LIBRARY_FOR_DISTRIBUTION=YES

輸入了這一長串命令後,Xcode會給你大量的輸出,當然,最重要的是這最後的結果:

命令行裡這兩個參數值得注意:

現在我們再創建一個模擬器適用的框架:

hzxu$ xcodebuild archive \
> -scheme PaleInk-iOS \
> -destination "generic/platform=iOS Simulator" \
> -archivePath ../Archive/PaleInk-Sim \
> SKIP_INSTALL=NO \
> BUILD_LIBRARY_FOR_DISTRIBUTION=YES

我們再為macOS創建同樣的框架:

hzxu$ xcodebuild archive \
> -scheme PaleInk-macOS \
> -destination "generic/platform=OS X" \
> -archivePath ../Archive/PaleInk-macOS \
> SKIP_INSTALL=NO \
> BUILD_LIBRARY_FOR_DISTRIBUTION=YES

成功後,Archive目錄裡就有我們創建的三個.xcarchive存檔文件了:

Archive目錄

好奇的話我們可以看看.xcarchive文件裡有那些東西:

看看Package裡的內容

最後我們來創建XCFramework,先來到Archive目錄:

hzxu$ pwd
/Users/hzxu/Projects/PaleInk/Archive

創建XCFramework需要提供每個framework的路徑,該路徑為.xcarchive文件包裡面Products/Library/Frameworks目錄下相應的.framework文件:

hzxu$ xcodebuild -create-xcframework \
> -framework ./PaleInk-iOS.xcarchive/Products/Library/Frameworks/PaleInk_iOS.framework \
> -framework ./PaleInk-Sim.xcarchive/Products/Library/Frameworks/PaleInk_iOS.framework \
> -framework ./PaleInk-macOS.xcarchive/Products/Library/Frameworks/PaleInk_macOS.framework \
> -output ./PaleInk.xcframework

成功後我們會看到:

xcframework successfully written out to: /Users/hzxu/Projects/PaleInk/Archive/PaleInk.xcframework

我們創建了一個包含多個framework的XCFramework框架,而多個framework分別對應iOS, macOS以及模擬器。在此基礎上,我們再來創建一個Swift package。

一步步創建Swift Package

Swift Package可以放在遠程,比如GitHub,因為package裡可以包含編譯好的二進制Binary Framework,為了不影響下載時間,二進制的binary framework可以以zip壓縮包的形式放在其他服務器上,而package裡只包含其URL。正如Cocoapod一樣,Swift Package也可以放在本地機器上,這種情況就需要package裡也包含二進制的binary framework了。

在XCode裡創建一個Swift Package,其一個簡單易懂的名字,比如PaleInkLocal。

建立Swift Package

默認情況下,你會看到XCode為你自動生成的一些文件:

Swift Package默認目錄結構以及文件

如果你不打算寫測試,也沒有什麼額外的要寫入PaleInkLocal.swift裡的,可以把不需要的文件刪除,最後把之前打包好的PaleInk.xcframework拖拽到Sources目錄下。注意:拖拽會把原本的PaleInk.xcframework移動到Swift Package相應的目錄下,而非複製。最後的效果如下:

刪減後的Swift Package目錄結構以及文件

我們還需要對Package.swift文件進行修改:

import PackageDescription

let package = Package(
    name: "PaleInkLocal",
    products: [
        .library(
            name: "PaleInk",
            targets: ["PaleInk"]),
    ],
    dependencies: [],
    targets: [
        .binaryTarget(name: "PaleInk", path: "./Sources/PaleInk.xcframework")
    ]
)

接下來我們創建一個新項目來使用這個Package,這裡我們其名為PaleInkDemo,並把剛剛創建的Package目錄整個拖到PaleInkDemo項目裡,效果如下:

本地Package拖到Demo項目裡

接下來,在項目設置的General → Frameworks, Libraries, and Embedded Content那裡增加PaleInk:

Package加入項目設置

在項目裡,我們導入PaleInk_iOS來使用剛剛創建的這個Package:

import PaleInk_iOS

我們簡單的做了一個Demo,在一個UITableView裡展示所有的色彩:

Demo

接下來,我們進一步看看怎麼把這個Package從本地弄到遠程。

遠程Swift Package

我們先來到之前創建xcframework的Archive目錄,把xcframework壓縮成zip文件:

hzxu$ zip -r PaleInk.xcframework.zip PaleInk.xcframework

之後我們建立一個新的Swift Package,這裡就叫PaleInk,按照之前PaleInkLocal的初試設置清理不需要的文件後,Package.swift文件內容修改如下:

import PackageDescription

let package = Package(
    name: "PaleInk",
    products: [
        // Products define the executables and libraries a package produces, and make them visible to other packages.
        .library(
            name: "PaleInk",
            targets: ["PaleInk"]),
    ],
    dependencies: [],
    targets: [
        .binaryTarget(name: "PaleInk", url: "https://www.haozhexu.me/SwiftPackages/PaleInk.xcframework.zip", checksum: "7c29c4a683a8102f2a8c11227f44bac4452dd0a38dd2b4ed34e1f808c6058080")
    ]
)

這裏面targets的binaryTarget,我們把xcframework放在了遠程,並且用蘋果提供的工具來計算checksum:

hzxu$ swift package compute-checksum ../Archive/PaleInk.xcframework.zip 
7c29c4a683a8102f2a8c11227f44bac4452dd0a38dd2b4ed34e1f808c6058080

接下來我們把這個Swift Package傳到GitHub,還是在打開剛剛建立的PaleInk這個Swift Package的狀態下,點擊Source Control → New Git Repositories…來為這個Swift Package建立一個新的Git Repo並且提交(commit)所有文件。

建立Git庫

有了本地的Git Repo,我們把它傳到Remote,打開Source Control (Cmd+2),移動鼠標到PaleInk上點右鍵,點New “PaleInk” Remote…

Git遠程庫

XCode會讓你選擇已有的或者新建一個Git帳號:

GitHub帳戶

填寫必要的信息後,點擊Create,接下来我们把package推到remote,右鍵點擊Package名,菜單裡點Tag “main”:

給main加標籤

在接下來的窗口中給Package指定一個tag,我們這裡就用它的版本:

標籤版本

最後我們把本地的改變給推到遠程:

推到遠端

最後我們來測試一下,打開剛才創建的PaleInkDemo,把之前添加的本地Package刪掉,注意,點Delete後XCode會問你是要把這個文件刪掉,還是只刪除文件的引用(Remove Reference),我們選擇後者,這樣就不會把原本在硬盤上的PaleInkLocal一起刪掉了。

刪除本地Package

我們通過Swift Package把PaleInk再重新加回來:

增加Package依賴

填寫必要的信息:

Package版本需求

最終PaleInk被加入到了項目裡:

Package加入成功

Package在項目裡的目錄結構如下:

Package在項目裡的目錄結構

延伸學習

至此,一個簡單的xcframework和Swift Package的創建就完成了,我們做的只能算是理想狀態,實際工作中一般會有各種複雜的情況。要想深入了解,最好的資源是蘋果的WWDC視頻以及文檔:

Binary Frameworks in Swift(Swift二進制框架): https://developer.apple.com/videos/play/wwdc2019/416/

Distribute binary frameworks as Swift packages(以Swift Package方式發布二進制框架): https://developer.apple.com/videos/play/wwdc2020/10147/

Create an XCFramework(創建XCFramework): https://help.apple.com/xcode/mac/current/#/dev544efab96

PaleInkDemo可以在GitHub上下載:https://github.com/haozhexu/PaleInkDemo