若要在同一個 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 上建置應用程式目標,再使用以下檢查步驟驗證範例的預期行為。
- 啟動 app。預覽應顯示
Sample Board和Columns: 1。 - 點選
Apply title。標題應變成Pebble Board,欄數則維持不變。 - 點選
Apply layout。欄數應變成Columns: 3,標題則維持不變。 - 再次點選任一按鈕。它應再次套用相同的合成值。
若要檢視型別界線,請暫時將第一個按鈕的處理器特化型別改為 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 型別相符本身並不能保證整數代表有用的欄數。
對於這項桌面任務,每個具型別的端點使用一個配接器,並由父檢視持有產生的狀態。如此一來,兩個元件便能透過同一個可重用的遵循宣告取得明確的完成契約,且每個結果的儲存位置都會在呼叫點清楚呈現。
來源
- 條件式遵循 — Swift Evolution
- 主要關聯型別的輕量同型別需求 — Swift Evolution
- 泛型 — The Swift Programming Language
- 閉包 — The Swift Programming Language
- 全域 actor — Swift Evolution
- 按鈕 — Apple Developer Documentation
- State — Apple Developer Documentation
- WindowGroup — Apple Developer Documentation