###### tags: `第14屆IT邦鐵人賽文章` # 【在 iOS 開發路上的大小事2-Day21】如何將自己寫的套件上傳到 Swift Package ## 前言 先前有寫過「[如何將自己寫的套件上傳到 CocoaPods](https://hackmd.io/@leoho0722/BJ5yy0e8c)」 剛好 Swift Package Manager 也是近年 iOS App 開始使用的第三方套件管理工具 所以就來研究一下,該如何製作一個 Swift Package ## 建立 Git Repo 就一般的建立 git repo,嗯對 README.md、.gitignore 都不用勾,只需要勾 LICENSE 因為後面在建立 Swift Package 的時候,會自己建立 ## 建立一個 Swift Package 建立 Swift Package 有兩種方法 1. Terminal 2. Xcode ### Terminal 首先,先在 Terminal cd 到要建立 Package 的目錄, 像是我要在桌面建立,那我就是 cd 到桌面 ``` mkdir <你幫 Package 取的名稱> # 這一步可透過手動建立資料夾來替代 cd <你幫 Package 取的名稱> swift package init --type <這個 Package 建立的類型> ``` ```swift package init --type``` 的 type 一共有四種類型 1. empty -> Sources 資料夾內什麼都沒有,Package.swift 需手動補齊,**無法編譯** 2. library -> 建立 iOS SDK 的話,用這個 (**推薦**) 3. executable 4. system-module #### empty ![](https://i.imgur.com/u6s5P2W.png) ▲ 使用 type empty 建立的資料夾結構 ![](https://i.imgur.com/ziBmKb5.png) ▲ 使用 type empty 建立的 Xcode 結構 #### library ![](https://i.imgur.com/6E0kb9Y.png) ▲ 使用 type library 建立的資料夾結構 ![](https://i.imgur.com/XDr3rNi.jpg) ▲ 使用 type library 建立的 Xcode 結構 #### executable ![](https://i.imgur.com/iaIp9JB.png) ▲ 使用 type executable 建立的資料夾結構 ![](https://i.imgur.com/onM1pZt.jpg) ▲ 使用 type executable 建立的 Xcode 結構 #### system-module ![](https://i.imgur.com/UGkTfdi.png) ▲ 使用 type system-module 建立的資料夾結構 ![](https://i.imgur.com/P8UTgPH.png) ▲ 使用 type system-module 建立的 Xcode 結構 ### Xcode 從 Xcode 新增有兩種方法,兩種方法建立出來的 Package type 都是 library 1. Files -> New -> Package ![](https://i.imgur.com/oz7MB1N.png) 2. Create a new Xcode project -> Multiplatform -> Swift Package ![](https://i.imgur.com/8HEiTCj.png) 幫這個 Package 取名稱,預設名稱為 MyLibrary ![](https://i.imgur.com/sMyF6MN.jpg) 按下 create 就會新增一個 Swift Package 了 ## 撰寫 Sources Code 使用 type library 或是 type executable 建立的 Swift Package 在 Sources 資料夾內都有預先建立好一個 swift 檔來告訴我們原始碼要放的位置 (type empty 也可以透過手動補齊的方式,變成 type library) type library 會在 Sources 資料夾內建立一個跟 Package 同名的資料夾,裡面會有一個跟 Package 同名的 swift 檔 ![](https://i.imgur.com/EIgMV8i.jpg) 以我的為例就是 MyFirstSwiftPackageLibrary.swift 裡面已經有先宣告一個與 Package 同名的 struct ```swift public struct MyFirstSwiftPackageLibrary { /// 這個變數的 Access Level /// - getter -> public /// - setter -> private /// - getter 跟 setter 同時指定時,只需要注明 setter 為 (set) public private(set) var text = "Hello, World!" /// 初始化 public init() { } } ``` 那我們來改寫一下,讓他包含我們要的 Function ```swift public struct MyFirstSwiftPackageLibrary { /// 這個變數的 Access Level /// - getter -> public /// - setter -> private /// - getter 跟 setter 同時指定時,只需要注明 setter 為 (set) public private(set) var text = "Hello, World!" /// 初始化 public init() { } public static func sayHello() { print("MyFirstSwiftPackageLibrary say Hello!") } public static func inputAndOutput(input: String?) { print("MyFirstSwiftPackageLibrary input \(String(describing: input)), and I output \(String(describing: input))") } } ``` 這邊我們新增了 func sayHello()、func inputAndOutput(input: String?) 來做示範 ## 撰寫 Package.swift 預設會長得像下面這樣 ```swift // swift-tools-version:5.5 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "MyFirstSwiftPackageLibrary", products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( name: "MyFirstSwiftPackageLibrary", targets: ["MyFirstSwiftPackageLibrary"]), ], 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: "MyFirstSwiftPackageLibrary", dependencies: []), .testTarget( name: "MyFirstSwiftPackageLibraryTests", dependencies: ["MyFirstSwiftPackageLibrary"]), ] ) ``` ### // swift-tools-version:5.5 我們看到第一行 ```// swift-tools-version:5.5``` 這邊的 5.5 指的是編譯時最低支援的 Swift 版本 後續有個 swiftLanguageVersions 參數可以指定相容的 Swift 版本 兩者之間必須以 ```// swift-tools-version:5.5``` 定義的版本為主 ``` Example 1: // swift-tools-version:5.5 swiftLanguageVersions: [.v5, .version(5.5)] ↑ 這樣可以 Build Succeeded,且無報錯 Example 2: // swift-tools-version:5.5 swiftLanguageVersions: [.version(5.5.1)] ↑ 這樣可以 Build Succeeded,但會報錯 因為上方定義的 swift-tools 版本為 5.5 但這邊指定相容的 swift 版本為 5.5.1 swift version 5.5 不相容 swift version 5.5.1 ``` ### Package 因完整參數較多,這邊主要以常用參數來介紹,完整參數請查看下面官方文件 [完整 Package 參數-Apple Developer Documentation](https://developer.apple.com/documentation/packagedescription/package/init(name:defaultlocalization:platforms:pkgconfig:providers:products:dependencies:targets:swiftlanguageversions:clanguagestandard:cxxlanguagestandard:)) 如果有寫錯的話,執行鍵旁邊的 Scheme 會消失,不讓你編譯! 這算是一種物理 debug??? ``` name -> 這個 Package 的名稱 platforms -> 這個 package 所支援的平台 products -> 這個 Package 要對外呈現的方式 dependencies -> 這個 Package 所依賴的其他套件 targets -> 這個 Package 的 target List swiftLanguageVersions -> 這個 Package 所相容的 Swift 版本 cLanguageStandard -> 這個 Package 所使用的 C 語言標準 cxxLanguageStandard -> 這個 Package 所使用的 C++ 語言標準 ``` #### platforms 這個 package 所支援的平台,像是 iOS、macOS、watchOS、tvOS、DriverKit、Linux 寫法如下 ```swift platforms: [ .iOS(.v14), .macOS(.v12), .tvOS(.v14), .watchOS(.v7), .driverKit(.v21) ] ``` #### products 這個 Package 要對外呈現的方式,有三種 1. Library -> 最常見的 2. Executable -> Package type 為 executable 3. Plugin -> 插件,像是 [Apple 的 SwiftDocCPlugin](https://github.com/apple/swift-docc-plugin) 寫法如下 ```swift products: [ .library( name: "MyFirstSwiftPackageLibrary", targets: ["MyFirstSwiftPackageLibrary"]), .executable( name: "MyFirstSwiftPackageLibrary", targets: ["MyFirstSwiftPackageLibrary"]), .plugin( name: "MyFirstSwiftPackageLibrary", targets: ["MyFirstSwiftPackageLibrary"]), ] ``` #### dependencies 這個 Package 所依賴的其他套件 這邊以之前所建立的 MyFirstPodLib 為例 寫法如下 ```swift dependencies: [ /// 指定版本 .package(url: "https://github.com/leoho0722/MyFirstPodLib.git", from: "0.0.4"), /// 指定版本區間 .package(url: "https://github.com/leoho0722/MyFirstPodLib.git", "0.0.1" ... "0.0.4"), /// 指定分支 .package(url: "https://github.com/leoho0722/MyFirstPodLib.git", branch: "master"), /// 指定 Commit .package(url: "https://github.com/leoho0722/MyFirstPodLib.git", revision: "172bc5cc5b92339f48346d549fc9212d50b1e68e"), /// 本地路徑 .package(path: "../MyFirstPodLib"), ] ``` #### targets 這個 Package 的 target List 因完整參數較多,這邊主要以常用參數來介紹,完整參數請查看下面官方文件 [完整 Target 參數-Apple Developer Documentation](https://developer.apple.com/documentation/packagedescription/target) ``` name -> Target 名稱 path -> Target 路徑,相依於 Package Root 預設為 [PackageRoot]/Sources/[TargetName] exclude -> 不想包含在 Target 內的檔案路徑 sources -> 在 Target 內的原始碼檔案路徑 預設 TargetName 底下都是原始碼檔案,採用遞歸搜尋 resources -> 在 Target 中的 Resources Files 像是 `Image Files`、`Xib Files`、`Text Files`、`Folder` 等 publicHeadersPath -> C 語言的公開標頭檔路徑 cSettings -> C 語言檔案編譯時的 Build Settings cxxSettings -> C 語言檔案編譯時的 Build Settings swiftSettings -> Swift 檔案編譯時的 Build Settings linkerSettings -> Package 內有用到的系統內建 Framework,要寫在這裡 ``` ##### resources 在 Target 中的 Resources Files **Swift tools version 5.3 起支援**將 Resources Files 一同打包到 Swift Package 內 下圖是**不需要**在 Package.swift 內標示的 Resources file types 因為 Xcode 能夠自動辨識其類型 ![](https://i.imgur.com/vKFPWs0.png) ▲ 圖截自 [WWDC 2020-Swift packages: Resources and localization](https://developer.apple.com/videos/play/wwdc2020/10169/) 下圖是**需要**在 Package.swift 內標示的 Resources file types 因為 Xcode 不能夠自動辨識其用途 ![](https://i.imgur.com/odKDt4C.png) ▲ 圖截自 [WWDC 2020-Swift packages: Resources and localization](https://developer.apple.com/videos/play/wwdc2020/10169/) 其中分為 func process() 與 func copy() ``` /// 會根據所使用的平台,對 Resouces Files 進行相對應的優化 /// 大部分的 Resources Files 都應該使用 process func process() # 推薦 /// Xcode 無法辨識用途的檔案、不根據平台進行優化,只進行拷貝 /// 如下圖的 Game Data 資料夾 func copy() ``` ![](https://i.imgur.com/MFTAStB.png) ▲ 圖截自 [WWDC 2020-Swift packages: Resources and localization](https://developer.apple.com/videos/play/wwdc2020/10169/) ##### linkerSettings Package 內有用到的系統內建 Framework,要寫在這裡 寫法如下 ```swift linkerSettings: [ .linkedFramework("UIKit", .when(platforms: [.iOS])), .linkedFramework("AppKit", .when(platforms: [.macOS])) ] ``` #### swiftLanguageVersions 這個 Package 所相容的 Swift 版本 這邊所定義的 Swift 版本不能大於最上方宣告的 ```// swift-tools-version:5.5``` 範例請看上方的 [// swift-tools-version:5.5](#-swift-tools-version55) 寫法如下 ```swift /// 單一版本 swiftLanguageVersions: [.v5] /// 多個版本 swiftLanguageVersions: [.v4_2, .v5] /// 指定版本 swiftLanguageVersions: [.version("5.5")] ``` ## 上傳 就一般的 git push、git tag,嗯對 這部分跟 CocoaPods 蠻不一樣的 ## 實際應用 在一個專案內 Add Package,貼上 [Git Repo 網址](https://github.com/leoho0722/MyFirstSwiftPackageLibrary) ![](https://i.imgur.com/HH7AsSt.jpg) 在要用的檔案 import MyFirstSwiftPackageLibrary ![](https://i.imgur.com/SoQQCzE.png) 執行結果 ![](https://i.imgur.com/08nfG7e.png) ## 新增套件的流程 這邊也整理一下新增套件的流程 ``` 步驟0:先建立一個要用來存放套件的 git repo 步驟1:在本地建立一個用來存放套件的資料夾 步驟2:用 Terminal 切到剛剛建立用來存放套件的資料夾目錄底下 步驟3:在 Terminal 輸入 swift package init --type <這個 Package 建立的類型> or Xcode File -> New -> Add Package 步驟4:打開 Package.swift (步驟3 用 Terminal 才需要) 步驟5:將 Source Code、圖片、Color Set 等資源放入 Sources/<Package 名稱> 資料夾內 步驟6:撰寫 Package.swift (看 Scheme 有沒有出現就知道有沒有寫錯了) 步驟7:Command+B 編譯,看是否成功編譯以及無錯誤 (target 裡面的 sources 路徑定義錯誤,會有警告訊息,要注意!!!!) 步驟8:將 Source Code 上傳到 git repo 步驟9:為剛剛上傳到 git repo 的套件新增 git tag 步驟10:測試是否可以在其他專案成功安裝 步驟11:(optional):修改 README.md ``` ## 更新套件的流程 有推出,就會有更新 所以這裡就要來講説,當要更新套件的時候,要怎麼來更新 更新流程如下 ``` 步驟1:更新 Source Code、圖片、Color Set 等檔案 步驟2:將更新過後的套件 git 到 git repo 步驟3:git tag <新版號> 步驟4:git push --tags,將新版號 push 到 git repo 步驟5:測試是否可以在專案內成功抓到新版本的套件 步驟6 (optional):修改 README.md ``` ## 參考資料 > 1. https://juejin.cn/post/6871489791213436941 > 2. https://www.appcoda.com.tw/swift-package-xcode/ > 3. https://developer.apple.com/documentation/packagedescription/package