Gib einer generischen SwiftUI-Komponente ein standardmäßiges Erscheinungsbild, indem du einen Initializer hinzufügst, der auf einen konkreten Inhaltstyp beschränkt ist. Behalte einen separaten Builder-Initializer für die Anpassung bei. In dieser Anleitung erstellen wir ein wiederverwendbares Panel für ein macOS-Fenster und erklären, wann die Inferenz von Standardargumenten in Swift 5.7 die Schnittstelle vereinfachen kann.
Definiere die beiden Aufrufe, die deine Komponente unterstützen soll
Die Aufgabe für Leserinnen und Leser ist einfach: Erstelle ein Desktop-Panel, das vollständig wirkt, wenn es nur mit einem Titel aufgerufen wird, und zugleich einem anderen Bildschirm ermöglicht, eigenen Inhalt bereitzustellen. Der umgebende Titel, die Abstände und der Rahmen sollen in beiden Fällen gleich bleiben.
Die vorgesehene Schnittstelle lautet DesktopPanel(title: "Workspace") für die Standarddarstellung und DesktopPanel(title: "Workspace") { Text("Three sample items") } für benutzerdefinierten Inhalt. Diese Aufrufe erstellen verschiedene Spezialisierungen derselben generischen Komponente.
Generische Parameter in Swift stehen für konkrete Typen. Eine Einschränkung wie Content: View verlangt die Konformität zu einem Protokoll; eine Gleichheitseinschränkung wie Content == DefaultPanelContent legt einen bestimmten Typ fest. Die Sprachreferenz dokumentiert sowohl diese Einschränkungen als auch die Auflösung überladener Initializer anhand von Constraints. Generische Parameter und Argumente
Damit hat die Komponente zwei klar abgegrenzte Aufgaben: Ihr body bestimmt das gemeinsame Erscheinungsbild des Panels, und ihr Initializer legt den darin platzierten Inhalt fest. Für den Standardinhalt erhält der Inhaltstyp einen Namen, damit der Komfort-Initializer auf eine stabile, gut lesbare Spezialisierung verweisen kann.
Erstelle ein vollständiges macOS-Beispiel
Verwende für diese Anleitung ein SwiftUI-App-Target für macOS mit einem Deployment Target von macOS 11 oder neuer und einem Compiler ab Swift 5.7. Die Mindestversion von macOS ist nötig für die unten verwendete Struktur mit App und WindowGroup; Apple beschreibt WindowGroup als Szenencontainer für die Ansichtshierarchie eines Fensters. WindowGroup
Ersetze den generierten App-Einstiegspunkt durch diese vollständige Quelldatei und entferne die generierte ContentView.swift, falls sie eine weitere ContentView deklariert. Im Target darf es nur eine @main-App-Deklaration geben. Alle unten gezeigten Werte sind synthetische lokale Beispiele.
import SwiftUI
struct DefaultPanelContent: View {
var body: some View {
VStack(alignment: .leading, spacing: 6) {
Text("Sample workspace")
.font(.headline)
Text("Choose a layout to begin.")
.foregroundColor(.secondary)
}
}
}
struct DesktopPanel<Content: View>: View {
private let title: String
private let content: Content
init(
title: String,
@ViewBuilder content: () -> Content
) {
self.title = title
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text(title)
.font(.title2)
Divider()
content
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.gray.opacity(0.08))
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(Color.gray, lineWidth: 1)
)
}
}
extension DesktopPanel where Content == DefaultPanelContent {
init(title: String) {
self.init(title: title) {
DefaultPanelContent()
}
}
}
struct ContentView: View {
var body: some View {
VStack(alignment: .leading, spacing: 20) {
DesktopPanel(title: "Default panel")
DesktopPanel(title: "Custom panel") {
HStack {
Text("Sample items")
Spacer()
Text("3")
.font(.headline)
}
Text("Layout: Compact")
.foregroundColor(.secondary)
}
}
.padding(24)
.frame(minWidth: 420, minHeight: 300)
}
}
@main
struct PanelExampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Erstelle das macOS-Target und führe es aus. Das erwartete Ergebnis sind zwei Panels mit derselben äußeren Gestaltung: Das erste enthält den Text des Standard-Arbeitsbereichs, das zweite eine Zählzeile, gefolgt von einer Layoutbeschreibung. In dieser Umgebung gibt es weder einen Swift-Compiler noch ein Apple-SDK, daher wurde das Beispiel geprüft, konnte hier aber nicht kompiliert werden.
Apples Anleitung zu benutzerdefinierten Ansichten unterstützt diese Zusammensetzung: Deklariere einen View-Konformer, implementiere dessen body und setze integrierte und benutzerdefinierte Ansichten zu einer Hierarchie zusammen. Sie erklärt außerdem, dass Modifizierer veränderte Ansichten zurückgeben. Deshalb gehört die gemeinsame Gestaltung um den Inhalt des Panels. Eine benutzerdefinierte Ansicht deklarieren
Warum der eingeschränkte Initializer den generischen Typ auflöst
Der benutzerdefinierte Initializer erhält einen Closure, der Content zurückgibt. Der Aufruf von content() erstellt den Wert, der im Panel gespeichert wird. Der Closure wird während der Initialisierung verwendet. Daher speichert diese Implementierung ihn nicht und benötigt kein @escaping.
Für den Aufruf ohne Builder stellt die Extension init(title:) gezielt dann bereit, wenn Content DefaultPanelContent ist. Diese Einschränkung liefert die fehlende Typinformation. Die Implementierung delegiert anschließend an den Builder-Initializer, damit die Properties nur an einer Stelle initialisiert werden.
Der resultierende Standardwert hat den Typ DesktopPanel<DefaultPanelContent>. Der Inhaltstyp des benutzerdefinierten Panels wird aus seinem Closure abgeleitet. Aufrufende müssen diesen potenziell komplizierten Typ nicht ausschreiben.
Eine nützliche Folge: Eine Standardinstanz ist bereits spezialisiert. Du kannst einer Variablen nicht einfach ein Panel mit einem anderen Inhaltstyp zuweisen, nur weil beide Werte Panels anzeigen. Wenn die Oberfläche alternative Darstellungen braucht, statte ihre umgebende Ansicht mit einer Builder-basierten Verzweigung aus.
Das Builder-Attribut ermöglicht den mehrzeiligen benutzerdefinierten Closure im Beispiel. Es erlaubt, dass HStack und das folgende Text zum erstellten Inhalt gehören; es ersetzt nicht die eigene Anforderung der Komponente Content: View. Apple dokumentiert, wie der Builder mehrere Ausdrücke und bedingte Inhalte verarbeitet. ViewBuilder
Wann kann ein einzelner Initializer mit Standardwert den Overload ersetzen?
Swift 5.7 hat SE-0347 implementiert. Damit kann ein konkreter Standardausdruck für ein ausgelassenes Argument einen generischen Typ ableiten. Für diese Komponente ergibt sich dadurch eine kürzere Alternative. SE-0347: Typinferenz aus Standardausdrücken
Bei einem Compiler ab Swift 5.7 ersetzt du den Builder-Initializer innerhalb von DesktopPanel durch das folgende Fragment und entfernst die gesamte eingeschränkte Extension. Behalte den Import, die gespeicherten Properties, DefaultPanelContent, body und den App-Code aus dem vollständigen Beispiel bei.
// Replacement initializer inside DesktopPanel.
init(
title: String,
@ViewBuilder content: () -> Content = {
DefaultPanelContent()
}
) {
self.title = title
self.content = content()
}
Wird der Closure weggelassen, wird Content nun aus DefaultPanelContent() abgeleitet. Wird stattdessen ein Closure angegeben, wird der Typ daraus abgeleitet. Das ist eine Inferenz anhand eines Standardausdrucks und kein Standardtypargument in der Komponentendeklaration.
Der Vorschlag schränkt diese Inferenz ein: Der betroffene generische Parameter darf in der Parameterliste nur an einer Stelle vorkommen, und damit verbundene Gleichheitseinschränkungen müssen die Regeln erfüllen. Hier erscheint Content nur im Builder-Parameter. Wird es in einem weiteren Initializer-Parameter wiederverwendet, kann dieser Ansatz ungültig werden. SE-0347: Detaillierter Entwurf
Wähle den Overload, wenn der Standardinhalt Eingaben des Initializers benötigt. Zum Beispiel kann ein Standarduntertitel, der aus title abgeleitet wird, innerhalb von init(title:) erstellt und an den Builder-Initializer übergeben werden. Ein Standardausdruck kann den übergebenen title nicht so erfassen, als wäre er ein im Rumpf dieses Initializers erstellter Closure.
| Anforderung an die Komponente | Geeignete Schnittstelle |
|---|---|
| Fester Standardinhalt und ein Compiler ab Swift 5.7 | Ein Builder-Parameter mit Standardwert ist knapp und übersichtlich. |
| Standardinhalt, der aus anderen Eingaben des Initializers erstellt wird | Verwende einen eingeschränkten Initializer und erstelle den Wert darin. |
| Mehrere benannte Standarddarstellungen | Verwende explizite Initializer-Overloads mit eindeutigen Labels und Einschränkungen. |
| Expliziter gespeicherter Typ für die Standardspezialisierung | Benenne DesktopPanel<DefaultPanelContent> oder führe einen Typalias ein. |
Versuche nicht, diese Schnittstelle als struct DesktopPanel<Content: View = DefaultPanelContent> auszudrücken. Die aktuelle Grammatik generischer Parameter in Swift sieht diese Standardtypsyntax nicht vor. Hier kommen eingeschränkte Initialisierung und Inferenz anhand von Standardausdrücken infrage. Generische Parameter und Argumente
Bewahre die Schnittstelle über verschiedene Builder-Versionen hinweg
Apples aktuelle Dokumentation unterscheidet zwischen Xcode 26 und älter sowie Xcode 27 und neuer. Mit der neueren Toolchain erzeugt ViewBuilder typenunabhängigen Inhalt; Apple führt ContentBuilder als Typalias für ViewBuilder ein, um dieses vereinheitlichte Verhalten zu bezeichnen. Diese Unterscheidung richtet sich nach der Build-Toolchain und nicht einfach nach dem Deployment Target der App. ViewBuilder, ContentBuilder
Das Beispiel behält @ViewBuilder bei, um mit älteren Toolchains kompatibel zu bleiben. Wenn du in Xcode 27 oder neuer die neue Schreibweise verwendest, ändere das Attribut dieses Parameters zu @ContentBuilder. Behalte Content: View bei: Das Panel benötigt weiterhin Inhalt, den es in einer Ansichtshierarchie platzieren kann.
Vermeide es, die Standardspezialisierung als verschachtelten Stack- und Tupeltyp anzugeben. Apples Migrationshinweis dokumentiert eine Änderung von TupleView zu TupleContent bei Buildern mit mehreren Ausdrücken. Sie kann Constraints brechen, die diese generierten Strukturen benennen. Das Beispiel schränkt die Extension stattdessen auf DefaultPanelContent ein; das interne Layout bleibt hinter body: some View verborgen. TN3211: Beheben von SwiftUI-Quellinkompatibilitäten für State und ContentBuilder
Dieser benannte Wrapper erleichtert auch gewöhnliche Änderungen. Wenn du dem Standardinhalt eine dritte Zeile hinzufügst, musst du dafür keinen langen konkreten Typ im Komfort-Initializer neu formulieren.
Prüfe den Vertrag, bevor du das Layout änderst
Wenn die Komponente nicht kompiliert, finde zunächst heraus, welchem Teil der Schnittstelle Typinformation fehlt. Abstände zu ändern oder alles in einen weiteren Stack einzupacken, löst keine fehlende generische Spezialisierung.
- Der Aufruf ohne Builder kann
Contentnicht ableiten. Prüfe, ob der eingeschränkteinit(title:)vorhanden ist oder ob der Initializer mit Standardwert mit Swift 5.7 oder neuer kompiliert wird. - Ein konkreter Standardwert kann
Contentnicht zugewiesen werden. Prüfe, ob die Zuweisung innerhalb der eingeschränkten Extension erfolgt. Die Konformität zuViewallein machtDefaultPanelContentnicht mit jedem möglichenContentaustauschbar. - Mehrere benutzerdefinierte Ausdrücke werden abgelehnt. Prüfe, ob der Closure-Parameter das Builder-Attribut trägt.
- Ein expliziter Typ lehnt benutzerdefinierten Inhalt ab. Eine Typannotation
DesktopPanel<DefaultPanelContent>legt den Inhaltstyp absichtlich fest. Entferne die Annotation, wenn der Aufruf eine andere Spezialisierung ableiten soll. - Ein Toolchain-Upgrade beschädigt ein tupelbasiertes Constraint. Ersetze die Abhängigkeit von der generierten Builder-Struktur durch eine benannte Standardansicht oder befolge Apples dokumentierte Migration für das jeweilige Constraint.
Halte den Builder schließlich schlank. In dieser Implementierung wird er bei jeder Initialisierung des Panels ausgeführt; eine einmalige Ausführung wird nicht garantiert. Apple weist darauf hin, dass SwiftUI Ansichten erneut initialisieren kann, und rät davon ab, umfangreiche Arbeit in ihren Initializern auszuführen. Eine benutzerdefinierte Ansicht deklarieren
Verwende den eingeschränkten Initializer als klaren Ausgangspunkt: Er macht die Standardspezialisierung sichtbar und lässt sich benutzerdefinierter Inhalt leicht ableiten. Übernimm den einzelnen Initializer mit Standardwert, wenn seine Compileranforderung und Inferenzbeschränkungen zu deiner API passen. Beide Ansätze lassen das macOS-Panel sein gemeinsames Erscheinungsbild verwalten, während Aufrufende nur den benötigten Inhalt bereitstellen.
Quellen
- Generische Parameter und Argumente — Swift.org
- Typinferenz aus Standardausdrücken — Swift Evolution
- Eine benutzerdefinierte Ansicht deklarieren — Apple Developer Documentation
- ViewBuilder — Apple Developer Documentation
- ContentBuilder — Apple Developer Documentation
- WindowGroup — Apple Developer Documentation
- TN3211: Beheben von SwiftUI-Quellinkompatibilitäten für State und ContentBuilder — Apple Developer Documentation