###### 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