Per permettere a un delegate di gestire diversi tipi di componenti SwiftUI, rendi il metodo di callback generico e mantieni il componente indipendente dal tipo concreto del delegate. Questa guida costruisce un piccolo selettore di tile macOS che preserva il tipo di contenuto del mittente senza un requisito Self ricorsivo.
Scegli il contratto di callback prima di scegliere la sintassi
Il compito del lettore è specifico: costruire un componente SwiftUI riutilizzabile il cui contenuto può variare, mentre un delegate riceve callback di selezione da ogni specializzazione di contenuto supportata. La dimostrazione usa due tile sintetici—testo e un cerchio—and visualizza l'identificatore del tile selezionato.
Swift definisce la delega come un modo basato su protocolli per una classe o struttura di delegare responsabilità a un'altra istanza. Un delegate può ricevere un'azione insieme al suo mittente, come mostrato nell'esempio di delega della guida del linguaggio. Protocols
Per questo componente, il contratto inteso è: "Per ogni tipo di contenuto conforme a View, il delegate può ricevere un tile contenente quel contenuto." Metti il parametro di tipo sul metodo di callback. Le funzioni generiche di Swift selezionano i loro argomenti di tipo concreti a ogni chiamata, mentre un associated type viene stabilito da una conformità al protocollo. Sono impegni diversi. Generics
| Requisito | Forma di interfaccia adatta | Conseguenza |
|---|---|---|
| Un delegate gestisce ogni tipo di contenuto supportato | Un metodo di callback generico | Ogni callback può avere un tipo di contenuto diverso. |
| Ogni delegate gestisce un tipo di contenuto fisso | Un associated type | Ogni conformità fornisce un tipo di contenuto. |
| Il callback ha bisogno solo di un identificatore di tile | Un callback che accetta l'identificatore | Il callback non ha dipendenze dal tipo di rendering. |
Questa tabella è un ausilio di progettazione derivato da quelle regole del linguaggio. Scegli la prima riga quando preservare il mittente tipizzato fa parte dell'API. Scegli la terza quando un identificatore di mittente fornisce tutto ciò di cui il ricevitore ha bisogno; trasportare un intero componente aggiungerebbe allora una dipendenza inutile.
Rimuovi la dipendenza dal tipo del delegate stesso
Un design strettamente accoppiato può rendere il componente generico sul suo delegate e poi riferirsi a quel componente come Component<Self> dentro il protocollo delegate. Qui, Self significa il tipo delegate conforme finale. Gli associated type appartengono allo stesso modo a quel tipo conforme. Declarations
Questa forma fa sì che il controllo del callback dipenda da una specializzazione del componente che a sua volta dipende dal delegate. Se una particolare dichiarazione ricorsiva venga accettata richiede l'esame dei suoi vincoli completi e dei diagnostici del compilatore. Sarebbe inaccurato dire che Swift rifiuta ogni relazione di protocollo ricorsiva: SE-0157 ha aggiunto vincoli ricorsivi sugli associated type in Swift 4.1. Support recursive constraints on associated types
Per il compito scelto, il tipo concreto del delegate non ha bisogno di apparire nel tipo del componente. Il design sotto dà a Tile un parametro generico, Content. Il suo delegate memorizzato è any TileDelegate, e il requisito del protocollo introduce il proprio parametro Content a livello di metodo. Il callback preserva il tipo di contenuto del mittente senza esprimerlo attraverso lo Self del delegate.
La scrittura any denota esplicitamente un valore esistenziale: il valore memorizzato può contenere un'istanza di un tipo delegate conforme. SE-0335 ha introdotto quella scrittura in Swift 5.6. Usare un esistenziale per il delegate non richiede di cancellare il contenuto del tile a AnyView. Introduce existential any
Costruisci un selettore di tile macOS completo
Usa un target app macOS SwiftUI con un compilatore Swift 6, come quello fornito con Xcode 16 o successivo. Imposta il target di deployment macOS a 11.0 o successivo per lo storage @StateObject di questo esempio. Apple documenta StateObject come disponibile da macOS 11.0. StateObject
Sostituisci la dichiarazione di app generata e la vista iniziale con questo singolo file sorgente. Assicurati che il target contenga solo una dichiarazione @main; Swift permette un solo approccio al punto di ingresso del programma. Declarations
import SwiftUI
import Combine
@MainActor
protocol TileDelegate: AnyObject {
func didChoose<Content: View>(_ tile: Tile<Content>)
}
@MainActor
struct Tile<Content: View>: View {
let id: String
let delegate: any TileDelegate
let content: Content
init(
id: String,
delegate: any TileDelegate,
@ViewBuilder content: () -> Content
) {
self.id = id
self.delegate = delegate
self.content = content()
}
var body: some View {
Button(action: {
delegate.didChoose(self)
}) {
VStack(spacing: 8) {
content
.frame(width: 100, height: 50)
Text("Choose \(id)")
}
.padding(8)
}
}
}
@MainActor
final class SelectionModel: ObservableObject, TileDelegate {
@Published private(set) var selection = "No tile selected"
func didChoose<Content: View>(_ tile: Tile<Content>) {
selection = "Selected: \(tile.id)"
}
}
@MainActor
struct ChooserView: View {
@StateObject private var model = SelectionModel()
var body: some View {
VStack(spacing: 16) {
HStack(spacing: 16) {
Tile(id: "text", delegate: model) {
Text("Sample")
}
Tile(id: "circle", delegate: model) {
Circle()
}
}
Text(model.selection)
}
.padding(24)
.frame(minWidth: 340, minHeight: 180)
}
}
@main
@MainActor
struct DelegateDemoApp: App {
var body: some Scene {
WindowGroup {
ChooserView()
}
}
}
Questo è un esempio completo di app, inclusi import, delegate, componente, modello e punto di ingresso. È stato ispezionato ma non compilato o eseguito qui perché l'ambiente disponibile non ha compilatore Swift o SDK macOS.
L'API Button di Apple accetta un'azione e una label che produce una vista. In questo esempio, cliccare un tile invoca il delegate dall'azione del button; valutare body costruisce l'interfaccia senza inviare un evento di selezione. Button
Compila ed esegui l'app su un Mac. Inizialmente, lo stato dovrebbe leggere No tile selected. Cliccare Choose text dovrebbe produrre Selected: text; cliccare Choose circle dovrebbe produrre Selected: circle. Questi sono risultati attesi dall'esempio, non risultati di esecuzione riportati.
Capire cosa preserva l'esempio
Il primo tile ha tipo Tile<Text>, e il secondo ha tipo Tile<Circle>. Entrambi usano la stessa istanza SelectionModel. Il suo callback generico soddisfa il requisito del protocollo per entrambi i tipi di contenuto, mentre legge la proprietà comune id. Questo è il beneficio pratico di mettere il parametro generico sul metodo.
Il ricevitore può anche accedere a tile.content come Content. Dentro l'implementazione generica, tuttavia, il vincolo dichiarato garantisce solo le capacità di View. Codice che richiede un'operazione specifica per il testo avrebbe bisogno di un contratto diverso o informazioni aggiuntive; il callback non può assumere che ogni valore di contenuto sia Text. Questo deriva dal modello generico basato sui vincoli di Swift. Generics
Il componente implementa body con some View, seguendo il pattern di vista personalizzata documentato da Apple. La dichiarazione View corrente porta anche l'isolamento main-actor. L'esempio marca esplicitamente il suo protocollo e i tipi rivolti all'UI @MainActor così che il callback e l'aggiornamento del modello condividano quell'isolamento. View
@StateObject fornisce uno storage modello stabile per la durata dell'identità della vista contenitrice. I cambiamenti alla selezione pubblicata del modello aggiornano la vista di stato dipendente. Spostare la costruzione del modello in un'espressione computata ordinaria non esprimerebbe più quella proprietà attraverso StateObject. StateObject
L'esempio memorizza un delegate richiesto fortemente. Il suo modello memorizza solo la stringa di selezione, quindi il grafo degli oggetti illustrato non ha riferimenti di ritorno a un tile. Se in seguito fai sì che il modello mantenga i mittenti, rivisita la proprietà. La guida del linguaggio di Apple dimostra un delegate legato a classe con storage weak quando quella relazione lo richiede. Protocols
Quando un associated type è il contratto migliore
Supponi che un ricevitore debba accettare solo tile di testo. Un protocollo alternativo potrebbe dichiarare associatedtype Content: View e richiedere func didChoose(_ tile: Tile<Content>). Questo esprime un tipo di contenuto per conformità. Rendere Content un associated type primario fornisce una sintassi di vincolo comoda; non trasforma il requisito in un metodo che accetta ogni tipo di contenuto. SE-0346 ha introdotto i primary associated type in Swift 5.7. Lightweight same-type requirements for primary associated types
Questa alternativa sarebbe una versione diversa dell'API. Dovresti anche cambiare la proprietà delegate del componente e il parametro dell'inizializzatore all'esistenziale vincolato corrispondente, come any FixedTileDelegate<Content>, e cambiare ogni ricevitore per fornire il suo associated type fisso e il corrispondente callback non generico. Una conformità solo-testo non potrebbe più servire il tile cerchio attraverso quella stessa conformità.
SE-0353 ha aggiunto i tipi esistenziali vincolati in Swift 5.7, permettendo agli associated type primari di un esistenziale di essere legati a tipi specificati. Legare un associated type descrive ancora una relazione fissa. Non fornisce una famiglia di implementazioni di callback per tipi di contenuto arbitrari. Constrained Existential Types
Mantieni il supporto del compilatore separato dal supporto di deployment. SE-0353 nota esplicitamente cambiamenti runtime che non sono retro-compatibili con le release OS esistenti. Prima di adottare l'alternativa su un target di deployment più vecchio, controlla i diagnostici di disponibilità nel target effettivo. L'esempio completo sopra usa un esistenziale delegate ordinario e non richiede storage di esistenziale vincolato. Constrained Existential Types
Diagnostica un adattamento fallito in un ordine utile
- Controlla lo scopo inteso del callback. Se un delegate deve gestire tile testo e cerchio, il suo witness deve mantenere la firma del metodo generico. Un metodo che accetta solo
Tile<Text>non soddisfa quel requisito. - Controlla i parametri generici del componente. Per questo design,
Tiledipende daContent; il suo tipo non include il tipo concreto del delegate. - Controlla l'isolamento dell'actor. Mantieni i callback UI e il modello observable illustrato sul main actor. Le chiamate da un altro contesto di isolamento devono rispettare quel confine.
- Controlla la posizione dell'evento. Invia la selezione dall'azione del button, non durante il calcolo di
bodyo l'inizializzazione di una vista. - Controlla la proprietà del modello. Preserva
@StateObjectnella vista contenitrice e evita di introdurre una seconda istanza del modello per il callback.
Se un adattamento fallisce ancora, riducilo a un tile e una implementazione delegate, poi ripristina il secondo tipo di contenuto. Quella sequenza distingue un problema di protocollo-witness da un problema di view-composition. I due diversi tipi di contenuto dell'esempio fanno un controllo di contratto utile: entrambi devono raggiungere lo stesso callback generico senza cast o cancellazione del contenuto.
La decisione di design essenziale è dove appartiene la variazione di tipo. Per un delegate che gestisce ogni specializzazione del componente, metti quella variazione sul metodo di callback e mantieni il tipo del componente indipendente dall'identità concreta del delegate. L'API risultante preserva un mittente tipizzato mantenendo il flusso di selezione macOS abbastanza piccolo da ispezionare ed estendere.
Fonti
- Generics — The Swift Programming Language
- Protocols — The Swift Programming Language
- Declarations — The Swift Programming Language
- Support recursive constraints on associated types — Swift Evolution
- Introduce existential any — Swift Evolution
- Lightweight same-type requirements for primary associated types — Swift Evolution
- Constrained Existential Types — Swift Evolution
- View — Apple Developer Documentation
- StateObject — Apple Developer Documentation
- Button — Apple Developer Documentation