關於 Swift Package Manager 的一些經驗分享

語言: CN / TW / HK

前言

Swift Package Manager 是 Apple 為了彌補當前 iOS 開發中缺少官方元件庫管理工具的產物。相較於其他元件管理控制元件,他的定義檔案更加輕鬆易懂,使用起來也很 Magic,只需將原始碼放入對應的資料夾內,Xcode 就會自動生成工程檔案,並生成編譯目標產物所需要的相關配置。同時,SPM 與 Cocoapods 相互相容,可以在特性上提供互補。

  • 專案地址:
  • https://github.com/apple/swift-package-manager
  • 相關文件:
  • https://developer.apple.com/documentation/swift_packages

這篇文章將主要介紹該元件管理器的現狀,常見使用方法,和將來的一些思考。

現狀

開源元件使用情況

檢視當前開源元件的 SPM 接入供應情況,不難發現幾乎全部還在維護的框架都支援使用這種方式整合。大到微軟的 APM SDK,小到介面 UI 元件,均有良好的相容支援。下面列舉一些可能會在後續開發中用到的元件。資源選自 https://github.com/ivanvorobei/awesome-ios 檢測規則為是否在倉庫主目錄下存在 Package.swift 檔案。

部分開源-iOS-框架專案-SPM-適配情況預覽表-Sheet12-1.png

統計中,56%左右的框架已經適配了 SPM 接入,且已經開始出現如 MarkdownUI 等框架僅適配 SPM 的情況。

一些優勢

  • 簡化的定義流程:將檔案放入約定的目錄內即可一鍵打包。
  • 簡化的 SPM 版本管理:Xcode 會根據定義檔案首行說明自動查詢相容的解決方案。
  • 簡化的上手流程:不需要安裝工具,也不需要命令列安裝元件。
  • 良好的持續整合能力:在完成專案配置以後,xcodebuild 無縫銜接,自動拉倉。
  • 良好的相容性:可與現有的大多陣列件管理方案混用。
  • 良好的除錯能力:斷點快狠準。

一些缺點

  • 文件難找。
  • 使用遠端倉庫對網路要求非常高。

使用方法

建立元件

建立元件可以在 Xcode 中選擇 Swift Package,也可以在命令列中寫入 swift package init。命令列建立會將當前目錄名稱用作包名。

Creating library package: Desktop Creating Package.swift Creating README.md Creating .gitignore Creating Sources/ Creating Sources/Desktop/Desktop.swift Creating Tests/ Creating Tests/DesktopTests/ Creating Tests/DesktopTests/DesktopTests.swift

定義元件

基礎的定義看起來長這個樣子。別急,我們一行一行來看。

// swift-tools-version:5.5

請勿忽略本行,當打包編譯出現工具鏈版本不匹配、 SDK 版本、系統 API 最低版本等問題時需要首先到這裡排查可能存在的問題。

``` // swift-tools-version:5.5

import PackageDescription

let package = Package( name: "MyLibrary", products: [ .library(name: "MyLibrary", targets: ["MyLibrary"]), ], dependencies: [ ], targets: [ .target(name: "MyLibrary", dependencies: []), .testTarget(name: "MyLibraryTests", dependencies: ["MyLibrary"]), ] ) ```

基礎定義

Swift Package 的定義稍微有一些繞,但是稍微解釋一下也就明瞭了。

Targets

先看 targets,定義是 A target can define a module or a test suite. 翻譯來說,就是一個 target 對應一個 clang module 或者 一個測試目標。一個 target 內只允許使用一類語言,比方說 Swift 或者 Objective-C/C/CPP。此處的 name 只對當前 package 可見,可以填寫在任意一個 dependencies 內。Target 支援 binary Target,可使用 XCFramwork 或 .a .so 等二進位制。

Products

再看 products,定義是 Products define the executables and libraries a package produces, and make them visible to other packages. 一個 product 可以包含多個 target,他們會被編譯成產物提供給專案。如果其他專案依賴當前的 Swift Package,此處的 name 可以填寫入其他 Package 的依賴需求內,一般對內不可用。最後來看一下 product 的幾種型別。一般來說,常見的 .library 可 type 包含 .static (預設) 和 .dynamic。除開 .library 還有 .executable 可選,用於編譯測試用二進位制和 macOS 命令列工具。

``` .library(name: "MorphingLabel", targets: ["MorphingLabel"]), .library(name: "MorphingLabelDynamic", type: .dynamic, targets: ["MorphingLabel"]),

.executable(name: "appdecrypt", targets: ["appdecrypt"]) ```

資原始檔

Swift Package 需要對每一個檔案指明用途。程式碼檔案會自動識別並編譯打包,資原始檔需要指定和說明。Swift Package 會為每一個 Package 生成一個 module 擴充套件,以便直接呼叫。使用命令列將專案檔案 Package.swift 轉換成 xcproj 則不會生成該模版定義檔案。以下定義會在 Bundle 類內生成 .module 屬性專門用於獲取 Particles 資料夾內的資源。

.target( name: "MorphingLabel", exclude: ["Info.plist", "tvOS-Info.plist"], resources: [ .process("Particles") ] // <-- 資原始檔 ),

目錄結構

Swift Package 推薦使用原生目錄結構,不推薦自定義 Path。

Swift Package 匯出標頭檔案有規定的位置,在當前 Source Path 內建立 include 會自動匯出。

Swift Package 需要對每一個資原始檔/資料夾顯示宣告,對萬用字元的適配存在 Bug。

當需要特定的檔案目錄組織的時候可以使用 符號連線 來連結目標檔案。

總體來說 Swift Package 中一個 Target 對應一個 name,而 專案根目錄/Sources/name 會作為當前 Target 的工作搜尋路徑。

``` . ├── Package.swift # 定義檔案 ├── README.md # 可忽略 ├── Sources # 此資料夾內全部檔案都需要定義 不然會報錯 │ └── demo # target demo 的預設目錄 │ ├── Particles. # 在 target 內宣告為資原始檔 │ │ └── fire.png # 會自動打包成 bundle 拷貝並傳遞 │ ├── demo.swift # target demo 的專案原始碼 │ └── include # 匯出標頭檔案 │ └── export.h # 標頭檔案

在 Sources / target name 釐頭的資原始檔可在 Package.swift 內定義。

需要在 target 內新增 resources: [ .process("Particles") ]

```

其他說明

XCFramework

關於編譯產物,基礎的 Swift Package 可以生成靜態庫、動態庫,在這以後可以手動打包成 XCFramework。SPM 的打包工作流對 XCFramework 非常友好,可以參考下面這個指令碼。

xcodebuild -create-xcframework \ -framework "$BUILD_FOLDER/iOS.xcarchive/Products/Library/Frameworks/MorphingLabel.framework" \ -framework "$BUILD_FOLDER/tvOS.xcarchive/Products/Library/Frameworks/MorphingLabel.framework" \ -framework "$BUILD_FOLDER/Simulator.xcarchive/Products/Library/Frameworks/MorphingLabel.framework" \ -framework "$BUILD_FOLDER/tvOSSimulator.xcarchive/Products/Library/Frameworks/MorphingLabel.framework" \ -output Build/LTMorphingLabel.xcframework

https://github.com/lexrus/LTMorphingLabel/blob/master/build_xcframework.sh

image.png

product -> .library(name: "MorphinglabelXCFramework", targets: ["LTMorphingLabel"]) // 需要手動打包,此處僅提供名稱給其他專案呼叫,依賴會使用二進位制庫 target -> .binaryTarget( name: "LTMorphingLabel", url: "https://github.com/lexrus/LTMorphingLabel/releases/download/0.9.3/LTMorphingLabel.xcframework.zip", checksum: "28a0ed8b7df12c763d45b7dde2aa41fd843984b79e6fbd3750f2fc1a6c247a13" )

目前有針對 Package.swift 生成並編譯 XCFrameowrk 的懶人工具,但是由於其依賴將專案轉換成 xcproj 的編譯方法,攜帶資原始檔的 Swift Package 並不能用。

https://github.com/akkyie/XPM

image.png

一些實踐

目前筆者有一個開源的私人專案使用了 SPM,可以拉下倉庫來看一看。Xcode 在解析各種依賴方面並不穩定,所以專案採用的方案是將所有程式碼拉到本地並通過修改 dependencies 的方式採用本地解析整合。本地整合的方式非常穩定,而且最大程度的保證了你修改原始碼的能力。Swift 發展非常快,目前不推薦 url 直接整合遠端倉庫。

https://github.com/SailyTeam/Saily

在本地建立 xcworkspace 以後便可以直接將 Package.swift 中的 product 新增到專案的編譯流程內。這裡再次讚賞 Swift Package 的多元相容,其中有一些庫是純 Objective-C 撰寫的,可以一鍵無縫整合。

本地整合的其他好處自然也包含 0 編譯警告,遇到任何問題你都可以直接打斷點到 Swift Package 的程式碼上。而 Cocoapod 經常不靈。關於編譯警告,養眼準備!

其中可以重點關注幾個混合編譯的庫的定義和 Fluent Icon 庫的定義檔案。其中就如上面描述的一樣,include 檔案會被自動匯出給 Swift 使用。

常見問題

Q: 我匯入了 Swift Package 到專案,但無法 import

A: 請 command + shift + K 清理專案重新編譯。Swift Package 有 module 快取。

Q: 我的 include 指向上級目錄的標頭檔案,匯出失敗了

A: 請清理專案重新編譯,有時需要重啟 Xcode。

Q: 我在編譯的時候指定了最低要求 iOS 13,為何 Swift Package 無法呼叫 API?

A: 請檢查 Package.swift 是否有在 platform 內指定版本,如有請升級 swift-tools-version 定義行。

Q: 我的資原始檔在新增 process 以後仍然有警告

A: 請使用資料夾名字或指定每一個檔案的名字,萬用字元並不能很好的工作。

Q: 我在定義 Package.swift 的時候沒有找到你說的這個幾個欄位

A: 請升級第一行的 swift-tools-version。

Q: 聯網拉取 Swift Package 無法完成

A: 請考慮清除 ~/Library/Caches/org.swift.swiftpm/,並換個好一些的網路。如果依然失敗請刪除 Package.resolved 檔案

如有問題可以新增評論補充。

後記

本人是十分喜歡 Swift Package 的,本地整合方便快捷,也給我很大的權力讓我所想落實到幾乎不可能落實的上游倉庫。配合 Swift Access Control,例如 module 內可訪問的 internal 屬性,很大程度上解決了寫 App 後臺的時候被 UI 意外呼叫造成的 crash,彌補上 Swift 沒有 class-private 訪問控制關鍵字的遺憾。除錯可以直接打到程式碼上,速度也很快。如果能為 Package 提供 .patch 的擴充套件檔案,再配合優化後的遠端倉庫,這將很有可能取代臃腫的 Cocoapod。pod 會修改編譯目標的 xcconfig,而 Swift Package 通過提供 library 和 workspace 的整合方式,侵入性非常低。最後,Swift Package 的多平臺編譯的能力也非常好,UIKit 一次編寫即可適配 iOS/iPadOS/tvOS/watchOS,編譯配置 CI 只需要呼叫 xcodebuild 即可自動解析,如有缺失自動拉取,省時省力。個人專案我可能不會再碰 Cocoapods。

由於 Swift Package 在世界範圍內的文件資源都非常稀缺,一旦出現問題,很難自行搜尋解決,會需要參考非常多已有開源專案的程式碼,知識點非常零散。如果有一些想法,請考慮給我們留言或者寫一寫評論。

加入我們

位元組跳動APM中臺目前致力於提升整個集團內全系產品的效能和穩定性表現,技術棧覆蓋iOS/Android/Flutter/Web/Hybrid/PC/遊戲/小程式等,工作內容包括但不限於線上監控,線上運維,深度優化,線下防劣化等。長期期望為業界輸出更多更有建設性的問題發現和深度優化手段。同時密切保持對業界前沿技術的關注,如Swift async/await,SwiftUI,Swift Package Manager等。

歡迎各位有識之士加入我們,一起為了“更快,更穩,更省,更有品質”的極致目標攜手前行。我們在北京,深圳兩地均有招聘需求,簡歷投遞郵箱: [email protected] ;郵件標題: 姓名 - 工作年限 - APM中臺 - 技術棧方向(如iOS/Android/Web/後端)。