SwiftUI 框架

一步步編寫模組化程式碼 在 SwiftUI 套用 Swift Package

Swift Package 是一個很好的工具,可以讓我們把程式碼分成一個個 Module,並在不同專案中使用。在這篇文章中,Rob 會簡單介紹如何在 SwiftUI 套用 Swift Package。
一步步編寫模組化程式碼 在 SwiftUI 套用 Swift Package
一步步編寫模組化程式碼 在 SwiftUI 套用 Swift Package
In: SwiftUI 框架
本篇原文(標題:Moving SwiftUI Views and Models Into Separate Swift Packages)刊登於作者 Medium,由 Rob Sturgeon 所著,並授權翻譯及轉載。

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。

但是,這樣並未能完全添加 MyViewsMyModels Package 到專案中。

我發現,我們必須添加 Package Module 到 target General 頁面的 Frameworks, Libraries, and Embedded Content 中,才可以匯入 Package Module。

Image by Rob Sturgeon

現在,我們終於可以在 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)、或是傳遞一個尾隨閉包,並在閉包中調用函式。

兩種方式都分別能夠創建這兩個按鈕,因為它們的程式碼是相同的。

我使用了 ContentViewModelMyModel.ContentViewModel。但實際上,你可以自行決定應該在 Package 中使用通用 (general) 還是更具體 (specific) 的 Module。

如果你不想用遠端儲存庫,就可以不用讀下去了!

創建及發佈到 GitHub 儲存庫

要把本地 Package 推送到遠端,最簡單的方法就是為 Package 建立 GitHub 儲存庫。

如果你有用過 GitHub CLI,應該會知道如何操作。不過,在這篇文章中,我會使用 GitHub Desktop App,因為我喜歡使用圖形使用者介面 (Graphical User Interface, GUI)。

下載並打開 GitHub Desktop,讓我們試著按下 command + O 來查看 MyViewsMyModels 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

如此一來,我們就可以確保我們是在使用遠端的版本,讓我們可以檢查所做的更改是否已成功推送。

本篇原文(標題:Moving SwiftUI Views and Models Into Separate Swift Packages)刊登於作者 Medium,由 Rob Sturgeon 所著,並授權翻譯及轉載。
作者簡介:Rob Sturgeon,iOS 開發者,撰寫有關 gadgets、初創公司和加密貨幣的文章。你可以在 Typesafely.substack 閱讀更多 Swift 編程的教學文章和 SwiftUI 文檔。
譯者簡介:Kelly Chan-AppCoda 編輯小姐。
作者
AppCoda 編輯團隊
此文章為客座或轉載文章,由作者授權刊登,AppCoda編輯團隊編輯。有關文章詳情,請參考文首或文末的簡介。
評論
很好! 你已成功註冊。
歡迎回來! 你已成功登入。
你已成功訂閱 AppCoda 中文版 電子報。
你的連結已失效。
成功! 請檢查你的電子郵件以獲取用於登入的連結。
好! 你的付費資料已更新。
你的付費方式並未更新。