Per ricevere completamenti di tipi diversi in una schermata SwiftUI per macOS, assegna a ogni componente un adattatore generico o una closure tipizzata. Swift non consente alla schermata di conformarsi due volte allo stesso protocollo con tipi associati diversi.
Questo tutorial realizza una piccola anteprima desktop i cui pulsanti applicano un titolo e un numero di colonne sintetici. L’obiettivo è mantenere il contratto di completamento di ogni componente verificato dal compilatore, consentendo al contempo a una vista genitore di ricevere entrambi i valori.
Perché un tipo non può adottare entrambi i contratti di completamento?
Il tipo associato di un protocollo appartiene alla sua conformità. Se un protocollo di completamento dichiara un tipo associato chiamato Value, il tipo conforme deve scegliere un valore coerente per quel tipo. Aggiungere due metodi di completamento sovraccaricati non crea due conformità indipendenti. La guida del linguaggio Swift spiega come i tipi associati acquisiscono i loro tipi concreti quando si adotta un protocollo. Generics — Associated Types.
Per esempio, una schermata non può adottare un protocollo di completamento una volta con Value uguale a TitleChoice e una seconda volta con Value uguale a LayoutChoice. Spostare le dichiarazioni in estensioni separate non cambia questa restrizione. SE-0143 mantiene esplicitamente il divieto di conformità ripetute, incluse quelle condizionali con condizioni disgiunte. Conditional conformances.
I tipi associati primari non eliminano questo limite. La sintassi CompletionHandling<TitleChoice> esprime un vincolo su un tipo associato; non crea un’identità di protocollo separata per ogni argomento. SE-0346 ha introdotto questa sintassi per i vincoli in Swift 5.7. Lightweight same-type requirements for primary associated types.
Il modello supportato usato qui sposta la conformità su un adattatore generico. C’è una sola dichiarazione di conformità e l’applicazione crea valori dell’adattatore specializzati in modo diverso per i due componenti. Il genitore possiede lo stato risultante senza adottare il protocollo di completamento.
Quale modello di completamento è adatto al tuo componente?
Prima di modificare l’interfaccia, usa il seguente supporto decisionale. Sono raccomandazioni di progettazione basate sulle regole documentate sui tipi, non ulteriori requisiti del linguaggio Swift.
| Requisito del componente | Modello suggerito | Cosa rimane esplicito |
|---|---|---|
| Un callback, con un’interfaccia che controlli | Una closure tipizzata | Il tipo di input del callback |
| Un’API esistente basata su protocollo | Un adattatore generico conforme | Il protocollo e il relativo tipo associato |
| Un canale di eventi che trasporta diverse alternative con nome | Un enum con valori associati | Ogni caso di evento gestito dal ricevitore |
| Diverse operazioni non correlate con significati diversi | Protocolli distinti con nome | I requisiti specifici di ogni operazione |
Per questo esempio, supponiamo che il componente riutilizzabile richieda già un gestore basato su protocollo. Un adattatore generico conserva quel confine e mantiene il codice di gestione della schermata vicino agli aggiornamenti di stato. Se il componente ha bisogno solo di un callback e non richiede un protocollo, una closure è la soluzione più semplice.
Un metodo di completamento generico esprimerebbe una promessa diversa. Un metodo come complete<T>(_ value: T) deve funzionare per ogni tipo consentito dai suoi vincoli. Non esprime il requisito più specifico secondo cui ogni componente fornisce un solo tipo di risultato particolare. La documentazione sui generics di Swift descrive come le funzioni generiche operano sui tipi scelti nei rispettivi punti di chiamata. Generics — Generic Functions.
Crea l’adattatore di completamento tipizzato
Per questo esempio, usa un target di applicazione SwiftUI per macOS con un compilatore Swift 5.7 o successivo e un target di distribuzione macOS 12 o successivo. Il requisito del compilatore deriva dalla dichiarazione del tipo associato primario. Il target di distribuzione è una base prudenziale per questo tutorial, non un’affermazione che ogni API qui mostrata sia apparsa per la prima volta in macOS 12.
I tre file seguenti compongono l’esempio completo. Aggiungili allo stesso target di applicazione, sostituendo il punto di ingresso e la vista del contenuto generati dall’app, così che il target abbia una sola dichiarazione @main. Non servono pacchetti, risorse, file di esempio o input esterni.
Crea CompletionContracts.swift con queste dichiarazioni:
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)
}
}
}
Il vincolo Handler.Value == Value collega l’output del componente all’input accettato dal gestore. Un pulsante che trasporta un TitleChoice richiede quindi un gestore il cui tipo associato sia TitleChoice. L’alias di tipo esplicito rende facile esaminare il tipo associato dell’adattatore.
L’adattatore conserva la closure per invocarla in seguito, quindi il suo inizializzatore contrassegna quel parametro con @escaping. La documentazione di Swift sulle closure richiede questa annotazione quando una closure può sopravvivere alla chiamata della funzione che la riceve. Closures — Escaping Closures.
Il protocollo, l’adattatore e la vista usano @MainActor, e il callback memorizzato mantiene tale isolamento nel proprio tipo funzione. In questo modo il contesto di esecuzione previsto per l’interfaccia utente diventa parte del contratto. SE-0316 documenta sia l’isolamento degli attori globali sia i tipi funzione qualificati da attori globali. Global actors.
Apple documenta Button come un’azione e un’etichetta. Qui l’azione invoca il gestore con il valore già tipizzato del componente; Text(title) fornisce l’etichetta visibile. Button.
Collega due tipi di risultato a una sola schermata SwiftUI
Crea ContentView.swift. I risultati del titolo e del layout hanno destinazioni di stato separate e ogni closure dell’adattatore aggiorna solo la destinazione corrispondente.
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)
}
}
Crea TypedCompletionsApp.swift come unico punto di ingresso dell’applicazione:
import SwiftUI
@main
@MainActor
struct TypedCompletionsApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Il genitore mantiene i valori iniziali in proprietà @State private. Apple documenta che SwiftUI gestisce questa archiviazione e aggiorna le viste dipendenti quando i valori cambiano. La documentazione attuale specifica anche che le build con Xcode 27 o successivo usano invece la macro State; lo stile di dichiarazione qui mostrato segue la precedente base con property wrapper. State.
WindowGroup ospita la gerarchia di viste della schermata. Apple documenta un’archiviazione di stato indipendente per ogni finestra creata dal gruppo, quindi in questo esempio lo stato appartiene a ciascuna finestra di anteprima. WindowGroup.
Ci sono due tipi di adattatore specializzati: CompletionAdapter<TitleChoice> e CompletionAdapter<LayoutChoice>. Entrambi derivano dall’unica dichiarazione di adattatore generico. ContentView si conforma solo a View; riceve i due risultati tramite le closure che fornisce.
Verifica il comportamento e individua le incompatibilità
Nell’ambiente di ricerca non è installato alcun compilatore Swift, quindi i file sono stati esaminati ma non compilati né eseguiti. Crea il target dell’applicazione sul tuo Mac, poi usa i controlli seguenti per verificare il comportamento previsto dell’esempio.
- Avvia l’app. L’anteprima dovrebbe mostrare
Sample BoardeColumns: 1. - Fai clic su
Apply title. Il titolo dovrebbe diventarePebble Board, mentre il numero di colonne rimane invariato. - Fai clic su
Apply layout. Il numero dovrebbe diventareColumns: 3, mentre il titolo scelto rimane invariato. - Fai di nuovo clic su uno dei pulsanti. Dovrebbe applicare di nuovo lo stesso valore sintetico.
Per esaminare il confine dei tipi, modifica temporaneamente la specializzazione del gestore del primo pulsante in CompletionAdapter<LayoutChoice> e assegna alla sua closure un corpo vuoto. Lascia il valore del pulsante come TitleChoice. Il componente richiede allora che tipi incompatibili siano uguali tramite Handler.Value == Value, quindi la compilazione dovrebbe fallire. Ripristina poi il gestore originale.
Se il compilatore segnala una conformità ripetuta, cerca un’altra dichiarazione che faccia adottare a uno stesso tipo nominale CompletionHandling. Mantieni la singola conformità dell’adattatore generico e crea un altro valore dell’adattatore per l’output aggiuntivo. Due estensioni vincolate che dichiarano la stessa conformità restano soggette alla restrizione di Swift sulle conformità ripetute. Conditional conformances — Multiple conformances.
Se il compilatore rifiuta la sintassi del tipo associato primario del protocollo, controlla la versione del compilatore. Con un compilatore meno recente, il modello dell’adattatore può mantenere una normale dichiarazione di tipo associato. Questo è un frammento sostitutivo solo per la dichiarazione del protocollo; lascia invariate le altre dichiarazioni:
@MainActor
protocol CompletionHandling {
associatedtype Value
func complete(_ value: Value)
}
Questa modifica elimina la dipendenza dell’esempio dalla sintassi introdotta in Swift 5.7. Non elimina il requisito del compilatore per @MainActor, introdotto in Swift 5.5, né modifica il target di distribuzione del tutorial. Il componente usa già una clausola generica where, quindi non ha bisogno della sintassi dei tipi associati primari nel punto in cui viene usato. Lightweight same-type requirements, Global actors.
Cosa garantisce questo contratto di completamento?
L’esempio garantisce un tipo di payload corrispondente al confine di ciascun componente. La sua implementazione chiama il gestore una volta per ogni attivazione del pulsante, in modo sincrono. Non implementa un’operazione asincrona, l’annullamento, la persistenza o un ciclo di vita del completamento «una volta sola». Questi aspetti richiederebbero comportamenti aggiuntivi e un contratto esplicito.
L’esempio usa inoltre valori locali noti. Se un futuro componente accetta numeri di colonne arbitrari, convalida l’input prima di aggiornare l’anteprima e definisci come segnalare il rifiuto. Il solo fatto che un tipo Swift corrisponda non garantisce che un intero sia un numero di colonne utile.
Per questo caso d’uso desktop, mantieni un adattatore per ogni endpoint tipizzato e lascia che il genitore possieda lo stato risultante. Così i due componenti hanno contratti di completamento precisi tramite un’unica dichiarazione di conformità riutilizzabile, e la destinazione di ciascun risultato è visibile nel punto di chiamata.
Fonti
- Conditional conformances — Swift Evolution
- Lightweight same-type requirements for primary associated types — Swift Evolution
- Generics — The Swift Programming Language
- Closures — The Swift Programming Language
- Global actors — Swift Evolution
- Button — Apple Developer Documentation
- State — Apple Developer Documentation
- WindowGroup — Apple Developer Documentation