Swift Package 是一個很好的工具,可以讓我們把程式碼分成一個個 Module,並在不同專案中使用。
這篇文章會簡單介紹如何在 SwiftUI 套用 Swift Package,不過其實這個方法適用於任何 Swift 程式碼。
建立 MyViews Package
打開 Xcode,點選 File > New > Package… 或按 Command、Control、Shift、和 N,來為這個專案建立一個新資料夾。如果你已經決定好要把專案放在哪裡,你也可以點擊 New folder 按鈕。我把資料夾命名為 PackagesExample
,不過名稱不太重要,你可以任意命名你的資料夾。讓我們把 Package 命名為 MyViews
,並確保它不會被添加到其他專案或 Workspace 中。
Package 中的 Sources 資料夾會為每個 target 提供單獨的資料夾。
讓我們在裡面創建兩個有 Swift 檔案的資料夾:
MyViews/Sources/MyViews/MyViews.swift
MyViews/Sources/HelloButton/HelloButton.swift
打開 HelloButton.swift
,並添加以下程式碼:
import SwiftUI
public struct HelloButton: View {
let action: () -> Void
public init(action: @escaping () -> Void) {
self.action = action
}
public var body: some View {
Button("Hello", action: action)
.buttonStyle(HelloButtonStyle())
}
}
public struct HelloButtonStyle: ButtonStyle {
public func makeBody(configuration: Configuration) -> some View {
Group {
configuration.label
.foregroundColor(.white)
.padding()
.background(Color.red)
.cornerRadius(15)
}
.frame(maxWidth: .infinity, alignment: .center)
}
}
這是一個簡單的按鈕,會有初始化程序中執行操作,就像普通的 SwiftUI Button
一樣。請注意,我們會把所有東西聲明為 public,希望在 Package Module 外都能夠引用這些內容。
接著我們會編寫 MyViews
,兩者其實十分相似。
import SwiftUI
public struct MyViews {
public struct HelloButton: View {
let action: () -> Void
public init(action: @escaping () -> Void) {
self.action = action
}
public var body: some View {
Button("Hello", action: action)
.buttonStyle(HelloButtonStyle())
}
}
public struct HelloButtonStyle: ButtonStyle {
public func makeBody(configuration: Configuration) -> some View {
Group {
configuration.label
.foregroundColor(.white)
.padding()
.background(Color.red)
.cornerRadius(15)
}
.frame(maxWidth: .infinity, alignment: .center)
}
}
}
你可能會想問,為什麼我要把相同的程式碼添加到兩個地方呢?
如此一來,我們就可以在 Swift Package 中有兩個選擇。
也就是說,我們就可以只 import HelloButton
,或是 import MyViews
,並利用 MyViews.HelloButton
引用它。
通常,我會選擇以兩種方法來構建 Package 的結構:建立一個 main target,當中包含像 HelloButton
這樣的型別;或是為每項內容分別創建獨立的 target。這兩種方法都各有好處。
如果建立了一個大 Module,我們就可以一次過匯入很多東西;但另一方面,我們又不想用一個 import
語句來存取所有東西。
我們需要在 Package.swift
檔案中指定所有 target 和 dependency,這些 target 和 dependency 應該在創建 Package 時已經創建好。
// swift-tools-version:5.5
import PackageDescription
// MARK: MyViews
let myViews = "MyViews"
let myViewsTarget: Target = .target(name: myViews)
let myViewsTargetDependency: Target.Dependency = .target(name: myViews)
// MARK: HelloButton
let helloButton = "HelloButton"
let helloButtonTarget: Target = Target.target(name: helloButton, dependencies: [myViewsTargetDependency])
let platforms: [SupportedPlatform]? = [.iOS(.v15)]
// MARK: Product
let libraryProduct: Product = .library(name: myViews, targets: [helloButton])
// MARK: Package
let package = Package(
name: myViews,
platforms: platforms,
products: [libraryProduct],
targets: [myViewsTarget, helloButtonTarget]
)
建立 MyModels Package
接下來,讓我們建立為視圖模型建立一個 Package,步驟其實是一樣的。
我們也可以只 import ContentViewModel
,或是 import
MyModels
,並利用 MyModels.ContentViewModel
引用它。
在 Xcode 點擊 File > New > Package…,或是按住 Command、Control、Shift、和 N。把 Package 命名為 MyModels
,並確保它不會被添加到其他專案或 Workspace 中。
Package 中的 Sources 資料夾會為每個 target 提供單獨的資料夾。
在裡面創建兩個有 Swift 檔案的資料夾:
MyModels/Sources/MyModels/MyModels.swift
MyModels/Sources/ContentViewModel/ContentViewModel.swift
打開 ContentViewModel.swift
並添加以下程式碼:
import Foundation
public class ContentViewModel: ObservableObject {
@Published public var hellos = 0
public init() {}
public func hello() {
hellos += 1
}
}
以上的程式碼非常簡單,當中有一個函式可以迭代 (iterate) 計算 HelloButton
被點擊的次數。
同樣地,MyModels.swift
就是這樣的:
import Foundation
public struct MyModels {
public init() {}
public class ContentViewModel: ObservableObject {
@Published public var hellos = 0
public init() {}
public func hello() {
hellos += 1
}
}
}
然後,我們一樣需要在 Package.swift
檔案中指定所有 target 和 dependency,這些 target 和 dependency 應該在創建 Package 時已經創建好。
import PackageDescription
// MARK: MyModels
let myModels = "MyModels"
let myModelsTarget: Target = .target(name: myModels)
let myModelsTargetDependency: Target.Dependency = .target(name: myModels)
// MARK: ContentViewModel
let contentViewModel = "ContentViewModel"
let contentViewModelTarget: Target = Target.target(name: contentViewModel,
dependencies: [myModelsTargetDependency])
let platforms: [SupportedPlatform]? = [.iOS(.v15)]
// MARK: Product
let libraryProduct: Product = .library(name: myModels, targets: [contentViewModel])
// MARK: Package
let package = Package(
name: myModels,
platforms: platforms,
products: [libraryProduct],
targets: [myModelsTarget, contentViewModelTarget]
)
整合程式碼
那我們要如何使用這些 Package 呢?在兩個 Package 的同一個根資料夾 (root folder) 中,讓我們建立一個新的 App 專案。我把專案命名為 PackagesExample
,與根資料夾一樣,你可以隨意命名專案。接著,讓我們點選 File > Add Packages…,並在視窗底部點擊 Add local…,來添加創建好的 Package。
但是,這樣並未能完全添加 MyViews
和 MyModels
Package 到專案中。
我發現,我們必須添加 Package Module 到 target General 頁面的 Frameworks, Libraries, and Embedded Content 中,才可以匯入 Package Module。
現在,我們終於可以在 ContentView.swift
整合整個 UI:
import SwiftUI
// General modules
import MyViews
import MyModels
// Specific modules
import HelloButton
import ContentViewModel
struct ContentView: View {
@ObservedObject var viewModel = MyModels.ContentViewModel()
@ObservedObject var viewModel2 = ContentViewModel()
var body: some View {
VStack {
HelloButton(action: viewModel.hello)
Text("Hellos: \(viewModel.hellos)")
.frame(maxWidth: .infinity, alignment: .center)
MyViews.HelloButton {
viewModel2.hello()
}
Text("Hellos: \(viewModel2.hellos)")
}
}
}
這是一個簡單的 VStack
,每個 HelloButton
下方都有一個 Text
,顯示它被點擊的次數。
為了讓範例更有趣,我用了兩種方式來創建這兩個按鈕:向函式傳遞直接引用 (direct reference)、或是傳遞一個尾隨閉包,並在閉包中調用函式。
兩種方式都分別能夠創建這兩個按鈕,因為它們的程式碼是相同的。
我使用了 ContentViewModel
和 MyModel.ContentViewModel
。但實際上,你可以自行決定應該在 Package 中使用通用 (general) 還是更具體 (specific) 的 Module。
如果你不想用遠端儲存庫,就可以不用讀下去了!
創建及發佈到 GitHub 儲存庫
要把本地 Package 推送到遠端,最簡單的方法就是為 Package 建立 GitHub 儲存庫。
如果你有用過 GitHub CLI,應該會知道如何操作。不過,在這篇文章中,我會使用 GitHub Desktop App,因為我喜歡使用圖形使用者介面 (Graphical User Interface, GUI)。
下載並打開 GitHub Desktop,讓我們試著按下 command + O 來查看 MyViews
或 MyModels
Package。你可能會收到一個 “this directory does not appear to be a Git repository. Would you like to create a repository here instead” 的訊息。
點擊 create a repository,如有需要,我們可以把 git ignore 更改為 Swift,並把其他選項保留為預設值。
如果你沒有收到以上訊息,就可以繼續添加儲存庫。
在創建儲存庫的時候,GitHub Desktop 就利用當前的程式碼創建了一個 initial commit。如果你在這一個步驟之後作出更改,就需要在這裡 commit。如果你已經滿意當前的 Package,就可以點擊右邊的 Publish repository 按鈕。
現在,Publish repository 按鈕旁邊應該會有一個 View on GitHub 按鈕,點擊 View on GitHub 後,就會彈出 GitHub 的網站。
複製 URL,然後回到我們的 App 專案(我剛剛把專案命名為 PackagesExample
)。
把本地 Package 換成遠端 Package
右擊或按住 Control 點擊我們剛剛在 File inspector 上載的 Package,點選 Delete,然後點選 Remove Reference。記得不要點選 Move To Trash!
現在我們已經移除了 Swift Package 的本地版本,讓我們到 File > Add Packages…,然後把剛剛從 Package 的 GitHub 複製的 URL 貼到右上角的搜尋方塊中。如果順利的話,應該就能成功添加 Package,而且因為有了正確的 Module,我們應該仍然可以 Build 專案。
重覆上述步驟,來添加另一個 Package。完成後,我們的專案就會有兩個遠端 Package,而沒有本地 Package。
有什麼分別呢?
你會發現,我們無法編輯 Package Dependencies 列表中的檔案。因為現在它們的版本是由 GitHub 儲存庫控制的,這就是 Package 一大優點。
如果我們無法編輯 Package,就肯定不會改到不屬於特定專案的程式碼了。
但因為這是我們的 Package,我們還是希望可以適時編輯程式碼。我們還是可以在 Xcode 打開 Package 的,但因為視圖和模型是分開的,我們無法看到完整的程式碼。
現在,如果我想編輯遠端 Package,最簡單的方法就是到 File > Add Packages... 並點擊 Add Local...,然後像實作時一樣添加 Package。
根據 Apple 的說法,本地 Package 一定會覆蓋遠端 Package。也就是說,無論遠端 Package 有甚麼更改,都會使用本地的 Moduel 版本。
同樣地,遠端存儲庫也不會因應本地存儲庫而更改,因此我們更改了本地存儲庫後,還是需要使用 GitHub Desktop 提交和推送到遠端(雖然大家可能比較常用源程式碼來控制)。
如果我們不會再更改本地 Package,就可以選擇 Remove Reference。
如此一來,我們就可以確保我們是在使用遠端的版本,讓我們可以檢查所做的更改是否已成功推送。