Damit ein Delegate mehrere SwiftUI-Komponententypen verarbeiten kann, sollte die Callback-Methode generisch sein und die Komponente unabhängig vom konkreten Typ des Delegates bleiben. Diese Anleitung erstellt einen kleinen macOS-Kachelauswähler, der den Inhaltstyp des Senders ohne rekursive Self-Anforderung bewahrt.
Den Callback-Vertrag festlegen, bevor die Syntax gewählt wird
Die Aufgabe ist konkret: Erstelle eine wiederverwendbare SwiftUI-Komponente, deren Inhalt variieren kann, während ein Delegate Auswahl-Callbacks für alle unterstützten Inhaltsspezialisierungen empfängt. Die Demonstration verwendet zwei künstliche Kacheln – eine mit Text und eine mit einem Kreis – und zeigt die Kennung der ausgewählten Kachel an.
Swift definiert Delegation als protokollbasierten Mechanismus, mit dem eine Klasse oder Struktur Aufgaben an eine andere Instanz übergibt. Ein Delegate kann eine Aktion zusammen mit ihrem Sender empfangen, wie im Delegationsbeispiel des Sprachleitfadens gezeigt. Protokolle
Für diese Komponente lautet der gewünschte Vertrag: „Für jeden Inhaltstyp, der View entspricht, kann das Delegate eine Kachel mit diesem Inhalt empfangen.“ Platziere den Typparameter auf der Callback-Methode. Generische Funktionen wählen ihre konkreten Typargumente bei jedem Aufruf aus, während ein assoziierter Typ durch eine Protokollkonformität festgelegt wird. Das sind unterschiedliche Festlegungen. Generics
| Anforderung | Geeignete Schnittstellenform | Konsequenz |
|---|---|---|
| Ein Delegate verarbeitet jeden unterstützten Inhaltstyp | Generische Callback-Methode | Jeder Callback kann einen anderen Inhaltstyp haben. |
| Jedes Delegate verarbeitet einen festen Inhaltstyp | Assoziierter Typ | Jede Konformität stellt einen Inhaltstyp bereit. |
| Der Callback benötigt nur die Kachelkennung | Callback mit der Kennung als Parameter | Der Callback hängt nicht vom Darstellungstyp ab. |
Diese Tabelle ist eine aus den Sprachregeln abgeleitete Designhilfe. Wähle die erste Zeile, wenn der typisierte Sender Teil der API sein soll. Wähle die dritte, wenn eine Senderkennung dem Empfänger alle benötigten Informationen liefert; die ganze Komponente mitzugeben, würde dann eine unnötige Abhängigkeit hinzufügen.
Die Abhängigkeit vom eigenen Typ des Delegates entfernen
Bei einem eng gekoppelten Entwurf kann die Komponente generisch über ihr Delegate sein und dann innerhalb des Delegate-Protokolls auf diese Komponente als Component<Self> verweisen. Hier steht Self für den später konformen Delegate-Typ. Auch assoziierte Typen gehören zu diesem konformen Typ. Deklarationen
Bei dieser Form hängt die Prüfung des Callbacks von einer Komponentenspezialisierung ab, die ihrerseits vom Delegate abhängt. Ob eine bestimmte rekursive Deklaration akzeptiert wird, lässt sich nur anhand ihrer vollständigen Einschränkungen und der Compilerdiagnosen beurteilen. Es wäre unzutreffend zu behaupten, Swift lehne jede rekursive Protokollbeziehung ab: SE-0157 führte in Swift 4.1 rekursive Einschränkungen für assoziierte Typen ein. Rekursive Einschränkungen für assoziierte Typen unterstützen
Für die gewählte Aufgabe muss der konkrete Typ des Delegates überhaupt nicht im Komponententyp vorkommen. Im folgenden Entwurf hat Tile einen generischen Parameter, Content. Sein gespeichertes Delegate hat den Typ any TileDelegate, und die Protokollanforderung führt einen eigenen Typparameter Content auf Methodenebene ein. Der Callback bewahrt den Inhaltstyp des Senders, ohne ihn über das Self des Delegates auszudrücken.
Die Schreibweise any bezeichnet ausdrücklich einen Existenzialwert: Der gespeicherte Wert kann eine Instanz eines konformen Delegate-Typs enthalten. SE-0335 führte diese Schreibweise in Swift 5.6 ein. Ein Existenzial für das Delegate erfordert nicht, dass der Inhalt der Kachel zu AnyView gelöscht wird. Existenzial-any einführen
Einen vollständigen macOS-Kachelauswähler erstellen
Verwende ein macOS-SwiftUI-App-Target mit einem Swift-6-Compiler, beispielsweise dem Compiler aus Xcode 16 oder höher. Setze das macOS-Bereitstellungsziel für den @StateObject-Speicher dieses Beispiels auf 11.0 oder höher. Apple dokumentiert, dass StateObject ab macOS 11.0 verfügbar ist. StateObject
Ersetze die generierte App-Deklaration und die Starter-Ansicht durch diese einzelne Quelldatei. Stelle sicher, dass das Target nur eine @main-App-Deklaration enthält; Swift erlaubt nur einen Ansatz für den Programmeinstiegspunkt. Deklarationen
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()
}
}
}
Dies ist ein vollständiges App-Beispiel mit Imports, Delegate, Komponente, Modell und Einstiegspunkt. Es wurde hier geprüft, aber nicht kompiliert oder ausgeführt, da die verfügbare Umgebung keinen Swift-Compiler und kein macOS-SDK enthält.
Apples Button-API akzeptiert eine Aktion und ein Label, das eine Ansicht erzeugt. In diesem Beispiel ruft ein Klick auf eine Kachel das Delegate in der Button-Aktion auf; die Auswertung von body erstellt die Benutzeroberfläche, ohne ein Auswahlereignis auszulösen. Button
Erstelle und starte die App auf einem Mac. Anfangs sollte der Status No tile selected lauten. Ein Klick auf Choose text sollte Selected: text anzeigen; ein Klick auf Choose circle sollte Selected: circle anzeigen. Dies sind erwartete Ergebnisse des Beispiels, keine berichteten Ausführungsergebnisse.
Verstehen, was das Beispiel bewahrt
Die erste Kachel hat den Typ Tile<Text>, die zweite den Typ Tile<Circle>. Beide verwenden dieselbe Instanz von SelectionModel. Dessen generischer Callback erfüllt die Protokollanforderung für beide Inhaltstypen und liest dabei die gemeinsame Eigenschaft id. Das ist der praktische Vorteil, den generischen Parameter auf Methodenebene zu platzieren.
Der Empfänger kann außerdem auf tile.content als Content zugreifen. In der generischen Implementierung garantiert die deklarierte Einschränkung jedoch nur die Fähigkeiten von View. Code, der eine textspezifische Operation benötigt, bräuchte einen anderen Vertrag oder zusätzliche Informationen; der Callback kann nicht voraussetzen, dass jeder Inhaltswert Text ist. Das folgt aus Swifts einschränkungsbasiertem Generics-Modell. Generics
Die Komponente implementiert body mit some View und folgt damit dem von Apple dokumentierten Muster für benutzerdefinierte Views. Die aktuelle View-Deklaration ist außerdem an den Main Actor gebunden. Das Beispiel markiert die UI-nahen Protokolle und Typen ausdrücklich mit @MainActor, damit Callback und Modellaktualisierung dieselbe Actor-Isolation verwenden. View
@StateObject stellt für die Lebensdauer der Identität der umschließenden View einen stabilen Modellspeicher bereit. Änderungen an der veröffentlichten Auswahl des Modells aktualisieren die abhängige Statusansicht. Würde die Modellinstanz in einen gewöhnlichen berechneten Ausdruck verschoben, würde diese Verwaltung nicht mehr durch StateObject ausgedrückt. StateObject
Das Beispiel speichert ein erforderliches Delegate stark. Sein Modell speichert nur die Auswahlzeichenfolge, daher enthält der dargestellte Objektgraph keine Referenz zurück auf eine Kachel. Wenn du das Modell später so änderst, dass es Sender speichert, solltest du die Besitzverhältnisse erneut prüfen. Apples Sprachleitfaden zeigt ein klassengebundenes Delegate mit schwacher Speicherung, wenn diese Beziehung das erfordert. Protokolle
Wann ein assoziierter Typ der bessere Vertrag ist
Angenommen, ein Empfänger soll nur Textkacheln akzeptieren. Ein alternatives Protokoll könnte associatedtype Content: View deklarieren und func didChoose(_ tile: Tile<Content>) verlangen. Damit wird ein Inhaltstyp pro Konformität festgelegt. Content zu einem primären assoziierten Typ zu machen, ermöglicht eine praktische Einschränkungssyntax; dadurch wird die Anforderung nicht zu einer Methode, die jeden Inhaltstyp akzeptiert. SE-0346 führte primäre assoziierte Typen in Swift 5.7 ein. Einfache Gleichheitstyp-Anforderungen für primäre assoziierte Typen
Diese Alternative wäre eine andere API-Version. Außerdem müsstest du die Delegate-Eigenschaft der Komponente und den Initializer-Parameter auf das passende eingeschränkte Existenzial ändern, etwa any FixedTileDelegate<Content>, und jeden Empfänger so ändern, dass er seinen festen assoziierten Typ und den dazugehörigen nicht generischen Callback bereitstellt. Eine reine Textkonformität könnte über dieselbe Konformität nicht mehr für die Kreiskachel verwendet werden.
SE-0353 ergänzte in Swift 5.7 eingeschränkte Existenzialtypen, mit denen sich die primären assoziierten Typen eines Existenzials an bestimmte Typen binden lassen. Die Bindung eines assoziierten Typs beschreibt weiterhin eine feste Beziehung. Sie stellt keine Familie von Callback-Implementierungen für beliebige Inhaltstypen bereit. Eingeschränkte Existenzialtypen
Halte Compilerunterstützung und Bereitstellungsunterstützung getrennt. SE-0353 weist ausdrücklich auf Laufzeitänderungen hin, die nicht rückwärtskompatibel auf bestehende Betriebssystemversionen bereitgestellt werden können. Bevor du die Alternative bei einem älteren Bereitstellungsziel verwendest, prüfe die Verfügbarkeitsdiagnosen im tatsächlichen Target. Das vollständige Beispiel oben verwendet ein gewöhnliches Delegate-Existenzial und benötigt keine Speicherung eingeschränkter Existenzialtypen. Eingeschränkte Existenzialtypen
Eine fehlerhafte Anpassung in einer sinnvollen Reihenfolge untersuchen
- Prüfe den vorgesehenen Geltungsbereich des Callbacks. Wenn ein Delegate Text- und Kreiskacheln verarbeiten muss, muss seine Witness-Methode die generische Methodensignatur beibehalten. Eine Methode, die nur
Tile<Text>akzeptiert, erfüllt diese Anforderung nicht. - Prüfe die generischen Parameter der Komponente. Bei diesem Entwurf hängt
TilevonContentab; sein Typ enthält nicht den konkreten Typ des Delegates. - Prüfe die Actor-Isolation. Lass UI-Callbacks und das dargestellte beobachtbare Modell auf dem Main Actor laufen. Aufrufe aus einem anderen Isolationskontext müssen diese Grenze beachten.
- Prüfe, wo das Ereignis ausgelöst wird. Löse die Auswahl in der Button-Aktion aus, nicht während
bodyberechnet oder eine View initialisiert wird. - Prüfe die Modellverwaltung. Behalte
@StateObjectin der umschließenden View bei und vermeide eine zweite Modellinstanz für den Callback.
Falls eine Anpassung weiterhin fehlschlägt, reduziere sie auf eine Kachel und eine Delegate-Implementierung und füge dann den zweiten Inhaltstyp wieder hinzu. So lässt sich ein Problem mit dem Protokoll-Witness von einem Problem mit der View-Komposition unterscheiden. Die zwei unterschiedlichen Inhaltstypen des Beispiels bieten eine nützliche Vertragsprüfung: Beide müssen denselben generischen Callback ohne Casts oder Löschen des Inhaltstyps erreichen.
Die entscheidende Designfrage ist, wo die Typvariation liegen soll. Bei einem Delegate, das jede Komponentenspezialisierung verarbeitet, gehört diese Variation auf die Callback-Methode; der Komponententyp bleibt unabhängig von der konkreten Identität des Delegates. Die resultierende API bewahrt einen typisierten Sender und hält den macOS-Auswahlablauf übersichtlich genug, um ihn zu prüfen und zu erweitern.
Quellen
- Generics — The Swift Programming Language
- Protokolle — The Swift Programming Language
- Deklarationen — The Swift Programming Language
- Rekursive Einschränkungen für assoziierte Typen unterstützen — Swift Evolution
- Existenzial-
anyeinführen — Swift Evolution - Einfache Gleichheitstyp-Anforderungen für primäre assoziierte Typen — Swift Evolution
- Eingeschränkte Existenzialtypen — Swift Evolution
- View — Apple Developer Documentation
- StateObject — Apple Developer Documentation
- Button — Apple Developer Documentation