若要在同一個 macOS SwiftUI 畫面中接收不同型別的完成結果,請為每個元件提供泛型配接器或具型別的閉包。Swift 不允許畫面以不同的關聯型別重複遵循同一個協定。

本教學會建立一個小型桌面預覽,其按鈕會套用合成的標題和欄數。目標是在讓單一父檢視接收兩種值的同時,讓編譯器檢查每個元件的完成契約。

為什麼同一個型別不能遵循兩種完成契約?

協定的關聯型別隸屬於該遵循。如果完成協定宣告名為 Value 的關聯型別,遵循該協定的型別就必須為它提供一致的選擇。新增兩個多載的完成方法,不會建立兩個獨立的遵循。Swift 語言指南說明了關聯型別在採用協定時如何取得其具體型別。泛型 — 關聯型別。

例如,畫面不能先遵循一次完成協定,令 Value 等於 TitleChoice,再以 Value 等於 LayoutChoice 遵循一次。將宣告移到不同擴充中也不會改變這項限制。SE-0143 明確保留了禁止重複遵循的規定,包括條件互斥的條件式遵循。條件式遵循。

主要關聯型別也無法消除此限制。語法 CompletionHandling<TitleChoice> 表示對關聯型別的約束;它不會針對每個引數建立不同的協定識別。SE-0346 在 Swift 5.7 中引入了這種約束語法。主要關聯型別的輕量同型別需求。

以下採用的支援設計,會將協定遵循移到泛型配接器上。遵循宣告只有一個,而應用程式會為兩個元件建立特化不同的配接器值。父檢視不必遵循完成協定,也能持有產生的狀態。

哪種完成設計適合你的元件?

變更介面前,請先參考以下決策指南。這些是根據已記載的型別規則提出的設計建議,並非額外的 Swift 語言要求。

元件需求 建議設計 明確保留的項目
一個回呼,且介面由你掌控 具型別的閉包 回呼的輸入型別
既有的協定式 API 泛型遵循配接器 協定及其關聯型別
一個事件通道承載多個具名選項 帶有關聯值的列舉 接收端要處理的每個事件案例
多個含義不同且彼此無關的操作 各自命名的協定 每個操作的不同需求

在本例中,假設可重用元件已經要求使用協定式處理器。泛型配接器保留了這個界線,也讓畫面中的處理程式碼可以與狀態更新放在一起。如果元件只需要回呼,沒有協定要求,使用閉包會是更精簡的設計。

泛型完成方法表達的是另一種承諾。像 complete<T>(_ value: T) 這樣的方法必須適用於其約束允許的每一種型別。它無法表達「每個元件只傳遞一種特定結果型別」這個較狹義的需求。Swift 泛型文件說明了泛型函式如何針對呼叫位置選定的型別運作。泛型 — 泛型函式。

建立具型別的完成配接器

本例使用 macOS SwiftUI 應用程式目標、Swift 5.7 或更新版本的編譯器,以及 macOS 12 或更新版本的部署目標。編譯器需求來自主要關聯型別宣告。部署目標是本教學採用的保守基準,並不表示本文中的每個 API 都是從 macOS 12 才開始提供。

以下三個檔案合起來構成完整範例。將它們加入同一個應用程式目標,並取代產生的 app 進入點和內容檢視,確保該目標只有一個 @main 宣告。不需要套件、資產、範例檔案或外部輸入。

建立 CompletionContracts.swift,加入以下宣告:

import SwiftUI

struct TitleChoice {
    let text: String
}

struct LayoutChoice {
    let columns: Int
}

@MainActor
protocol CompletionHandling<Value> {
    associatedtype Value
    func complete(_ value: Value)
}

@MainActor
struct CompletionAdapter<Output>: CompletionHandling {
    typealias Value = Output

    private let receive: @MainActor (Output) -> Void

    init(_ receive: @escaping @MainActor (Output) -> Void) {
        self.receive = receive
    }

    func complete(_ value: Output) {
        receive(value)
    }
}

@MainActor
struct CompletionButton<Value, Handler: CompletionHandling>: View
where Handler.Value == Value {
    let title: String
    let value: Value
    let handler: Handler

    var body: some View {
        Button(action: {
            handler.complete(value)
        }) {
            Text(title)
        }
    }
}

約束 Handler.Value == Value 會將元件的輸出連結到處理器接受的輸入。因此,承載 TitleChoice 的按鈕需要關聯型別為 TitleChoice 的處理器。明確的型別別名讓配接器的關聯型別更容易檢視。

配接器會儲存閉包以供稍後呼叫,因此其初始化器將該參數標記為 @escaping。Swift 閉包文件要求:如果閉包的生命週期可能超出接收它的函式呼叫,就必須加上這個標註。閉包 — 逃逸閉包。

協定、配接器和檢視都使用 @MainActor,而儲存的回呼也在函式型別中保留這項隔離。如此一來,預期的 UI 執行環境便成為契約的一部分。SE-0316 說明了全域 actor 隔離,以及帶有全域 actor 限定的函式型別。全域 actor。

Apple 說明 Button 會接收一個動作和一個標籤。這裡的動作會使用元件已具型別的值呼叫處理器;Text(title) 則提供按鈕上顯示的標籤。按鈕。

將兩種結果型別連接到同一個 SwiftUI 畫面

建立 ContentView.swift。標題和版面配置結果分別有自己的狀態儲存位置,而每個配接器閉包只會更新對應的位置。

import SwiftUI

@MainActor
struct ContentView: View {
    @State private var previewTitle = "Sample Board"
    @State private var columnCount = 1

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("Component Preview")
                .font(.headline)

            Text(previewTitle)
            Text("Columns: \(columnCount)")

            HStack {
                CompletionButton(
                    title: "Apply title",
                    value: TitleChoice(text: "Pebble Board"),
                    handler: CompletionAdapter<TitleChoice> { choice in
                        previewTitle = choice.text
                    }
                )

                CompletionButton(
                    title: "Apply layout",
                    value: LayoutChoice(columns: 3),
                    handler: CompletionAdapter<LayoutChoice> { choice in
                        columnCount = choice.columns
                    }
                )
            }
        }
        .padding(24)
        .frame(minWidth: 420, minHeight: 180)
    }
}

建立 TypedCompletionsApp.swift,作為應用程式唯一的進入點:

import SwiftUI

@main
@MainActor
struct TypedCompletionsApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

父檢視會將初始值存放在私有的 @State 屬性中。Apple 說明 SwiftUI 會管理這些儲存空間,並在值變更時更新相依檢視。目前的文件也提到,使用 Xcode 27 或更新版本建置時會改用 State 巨集;此處的宣告寫法遵循較早期的屬性包裝器基準。State。

WindowGroup 會承載畫面的檢視階層。Apple 說明該群組建立的每個視窗都有獨立的狀態儲存,因此本例的狀態各自屬於每個預覽視窗。WindowGroup。

這裡有兩種特化的配接器型別:CompletionAdapter<TitleChoice> 和 CompletionAdapter<LayoutChoice>。兩者都來自同一個泛型配接器宣告。ContentView 只遵循 View;它透過提供的閉包接收兩種結果。

驗證行為並診斷型別不符

研究環境中沒有安裝 Swift 編譯器,因此這些檔案僅經過檢視,並未編譯或執行。請在你的 Mac 上建置應用程式目標,再使用以下檢查步驟驗證範例的預期行為。

  1. 啟動 app。預覽應顯示 Sample Board 和 Columns: 1。
  2. 點選 Apply title。標題應變成 Pebble Board,欄數則維持不變。
  3. 點選 Apply layout。欄數應變成 Columns: 3,標題則維持不變。
  4. 再次點選任一按鈕。它應再次套用相同的合成值。

若要檢視型別界線,請暫時將第一個按鈕的處理器特化型別改為 CompletionAdapter<LayoutChoice>,並為其閉包指定空函式本體。保留該按鈕的值為 TitleChoice。此時元件會透過 Handler.Value == Value 要求不相容的型別必須相等,因此編譯應會失敗。之後請還原原本的處理器。

若編譯器回報重複遵循,請找出是否有其他宣告讓相同的名義型別遵循 CompletionHandling。保留唯一的泛型配接器遵循,並為額外輸出建立另一個配接器值。即使兩個受約束的擴充宣告相同的遵循,仍受 Swift 的重複遵循限制。條件式遵循 — 多重遵循。

若編譯器不接受協定的主要關聯型別語法,請檢查編譯器版本。對較舊的編譯器,配接器設計仍可使用一般的關聯型別宣告。以下片段只會取代協定宣告;其他宣告維持不變:

@MainActor
protocol CompletionHandling {
    associatedtype Value
    func complete(_ value: Value)
}

此變更會移除此範例對 Swift 5.7 語法的依賴。它不會移除 @MainActor 的編譯器需求;該功能是在 Swift 5.5 中引入,也不會變更本教學的部署目標。元件已使用泛型 where 子句,因此在使用位置不需要主要關聯型別語法。輕量同型別需求、全域 actor。

這個完成契約保證什麼?

本範例保證每個元件邊界的負載型別相符。每次按鈕啟動時,實作都會同步呼叫處理器一次。它沒有實作非同步操作、取消、持久化或只完成一次的生命週期。這些行為需要額外的實作和明確的契約。

本範例也使用已知的本機值。如果未來的元件接受任意欄數,請在更新預覽前驗證輸入,並定義拒絕時要如何回報。Swift 型別相符本身並不能保證整數代表有用的欄數。

對於這項桌面任務,每個具型別的端點使用一個配接器,並由父檢視持有產生的狀態。如此一來,兩個元件便能透過同一個可重用的遵循宣告取得明確的完成契約,且每個結果的儲存位置都會在呼叫點清楚呈現。

來源