Deklariere von Aufrufern bereitgestellte Inhalte als (Input) -> Content, wobei Content auf View beschränkt ist. So kann eine macOS-Komponente einen typisierten Wert bereitstellen, während die Aufrufer dessen Darstellung wählen. Der Vertrag verlangt, diese Eingabe entgegenzunehmen; er kann nicht verlangen, sie zu verwenden.

In diesem Tutorial wird eine kleine Desktop-Vorschau für Farbfelder erstellt. Die wiederverwendbare Komponente besitzt eine Größensteuerung und übergibt der Rendering-Closure des Aufrufers eine synthetische Konfiguration. Der Aufrufer zeichnet das Farbfeld, ohne den internen Zustand der Komponente zu verwalten.

Welche Schnittstelle drückt die Anforderung aus?

Verwende einen benannten generischen Parameter für die zurückgegebene View und einen konkreten Typ für die Eingabe der Komponente. Im folgenden Beispiel lautet die wesentliche Signatur @ViewBuilder content: @escaping (SwatchInput) -> Content innerhalb einer Deklaration mit der Einschränkung Content: View.

Eingabe und Ausgabe erfüllen unterschiedliche Aufgaben. SwatchInput definiert die Werte, die die Komponente bereitstellt. Content steht für den konkreten View-Typ, den die Closure jedes Aufrufers erzeugt. Benannte generische Parameter ermöglichen es einer Deklaration, diese Typbeziehungen auszudrücken und wiederzuverwenden, wie im Swift-Kapitel Generics beschrieben.

Apple verwendet dieses Schnittstellenmuster in GeometryReader: Sein Initializer akzeptiert eine Closure vom Typ (GeometryProxy) -> Content. Der Container stellt den Proxy bereit, und der Aufrufer erstellt daraus den Inhalt. Das ist ein nützliches Vorbild für eine benutzerdefinierte Komponente, die ihre eigene Konfiguration bereitstellt. Siehe Apples Dokumentation zu GeometryReader und init(content:).

Wähle den Vertrag entsprechend der tatsächlichen Aufgabe:

Anforderung Geeignete Schnittstelle
Der Aufrufer stellt Inhalte ohne Eingabe der Komponente bereit () -> Content
Die Komponente stellt eine bestimmte Konfiguration bereit (SwatchInput) -> Content
Der Container erhält einen Wert, dessen Typ je nach Verwendung variiert Ein benannter generischer Parameter Input mit einer passenden (Input) -> Content-Closure
Der Compiler muss nachweisen, dass der Aufrufer die Konfiguration verwendet Eine Closure-Signatur drückt diese Anforderung nicht aus

Diese Auswahl ergibt sich aus den Funktionstypen von Swift: Ihre Parameter- und Rückgabetypen beschreiben, was übergeben und zurückgegeben werden kann. Sie beschreiben nicht, wie die Implementierung ihre Argumente verwenden muss. Siehe den Abschnitt Function Type in der Sprachreferenz von Swift.

Eine vollständige macOS-Vorschau für Farbfelder erstellen

Verwende ein macOS-App-Ziel mit SwiftUI. Das Beispiel nutzt die etablierte Schreibweise @ViewBuilder und ist für Swift 5.7 oder neuer mit einem macOS-12- oder neueren Deployment-Ziel gedacht. Es kommt ohne Preview-Makros und neuere Namen für Content-Builder aus. Swift und das Apple SDK sind in der Rechercheumgebung nicht verfügbar; daher wurde dieses Programm geprüft, hier aber weder kompiliert noch ausgeführt.

Ersetze in einem Xcode-SwiftUI-App-Projekt die generierte App-Deklaration durch das folgende vollständige Programm. Entferne die generierte Deklaration ContentView, falls sie sonst doppelt vorhanden wäre. Behalte genau einen mit @main markierten App-Einstiegspunkt bei. Apples Xcode-Hilfe beschreibt, wie du mit einem SwiftUI-Projekt beginnst oder einer bestehenden App eine SwiftUI-Datei hinzufügst.

import SwiftUI

struct SwatchInput {
    let title: String
    let side: CGFloat
    let color: Color
}

@MainActor
struct SwatchPanel<Content: View>: View {
    @State private var isLarge = false
    private let content: (SwatchInput) -> Content

    init(
        @ViewBuilder content: @escaping (SwatchInput) -> Content
    ) {
        self.content = content
    }

    private var input: SwatchInput {
        SwatchInput(
            title: "Sample A",
            side: isLarge ? 120 : 64,
            color: .blue
        )
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Toggle("Large sample", isOn: $isLarge)
            content(input)
        }
        .padding(24)
    }
}

@MainActor
struct ContentView: View {
    var body: some View {
        SwatchPanel { input in
            Text(input.title)
                .font(.headline)

            RoundedRectangle(cornerRadius: 12)
                .fill(input.color)
                .frame(width: input.side, height: input.side)

            if input.side == 120 {
                Text("Large preview")
                    .font(.caption)
            }
        }
        .frame(width: 300, height: 280, alignment: .topLeading)
    }
}

@main
@MainActor
struct TypedContentDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Die erwartete Anfangsanzeige enthält den Schalter Large sample, den Titel Sample A und ein blaues abgerundetes Quadrat mit einer Seitenlänge von 64 Punkten. Wird der Schalter aktiviert, ändert sich die bereitgestellte Seitenlänge auf 120 Punkte und Large preview wird hinzugefügt. Diese Größen und Beschriftungen sind veranschaulichende lokale Werte, keine Messergebnisse.

Die Komponente besitzt isLarge. Die berechnete Eigenschaft input fasst die aktuelle Auswahl zusammen, und content(input) übergibt dieses Paket an den Aufrufer. SwiftUI verwaltet den Zustandsspeicher und aktualisiert die Teile der View-Hierarchie, die von einem geänderten Wert abhängen, wie unter State dokumentiert.

Der Aufrufer bestimmt die Darstellung: einen Titel, eine Form und eine optionale Beschriftung. Er kann eine andere Anordnung einsetzen und dabei dieselbe typisierte Eingabe verwenden. Sowohl das Panel als auch sein Aufrufer implementieren body, um ihre Schnittstellen zusammenzusetzen, gemäß Apples dokumentiertem View-Modell.

Warum sind die Builder- und Escaping-Annotationen erforderlich?

Mit @ViewBuilder kann der Aufrufer mehrere View-Ausdrücke und unterstützte bedingte Zweige in einer Closure angeben. In diesem Beispiel stellt der Aufrufer zwei unbedingte Views und eine bedingte Beschriftung bereit. Der Builder fügt diesen Inhalt zu einem Ergebnis zusammen, das die Rückgabebeschränkung der Komponente erfüllt. Apple dokumentiert mehrere untergeordnete Views und bedingte Konstruktionen unter ViewBuilder.

Der Builder stellt SwatchInput nicht bereit. Der gewöhnliche Closure-Parameter deklariert die Eingabe, und content(input) liefert das tatsächliche Argument. Würde der Initializer () -> Content akzeptieren, würde eine Builder-Annotation daraus keine Closure machen, die eine Konfiguration entgegennimmt.

@escaping ist erforderlich, weil der Initializer die Closure in einer Eigenschaft speichert, um sie später von body aus aufzurufen. Das Swift-Kapitel Closures erklärt, dass eine Closure escaped, wenn sie aufgerufen werden kann, nachdem die empfangende Funktion zurückgekehrt ist.

Bei einer Komponente, deren Konfiguration beim Initialisieren bereits feststeht, besteht eine weitere Möglichkeit darin, einen nicht escapenden Builder sofort aufzurufen und den resultierenden Content-Wert zu speichern. Dadurch ändert sich, wann die Konfiguration angewendet wird. Hier wird die Closure gespeichert, um den beabsichtigten Ablauf klar zu machen: Die aktuelle Eingabe wird aus dem Komponentenstatus abgeleitet, anschließend wird damit der Inhalt erstellt.

Die Builder-Grenze von Xcode beachten

Apples aktuelle Dokumentation unterscheidet zwischen Xcode 26 und älteren Versionen sowie Xcode 27 und neueren Versionen. Für die älteren Tools beschreibt sie ViewBuilder als Builder für Views. Für Xcode 27 und neuer beschreibt sie eine typunabhängige Konstruktion und empfiehlt den Alias ContentBuilder, um dieses breitere Verhalten kenntlich zu machen. Siehe ViewBuilder.

Bei der Pflege einer Codebasis mit Xcode 27 oder neuer kann der Initializer stattdessen @ContentBuilder verwenden. Apple dokumentiert dies als Typalias für ViewBuilder; die Konformität wird anhand der erzeugten Inhaltstypen sichergestellt. Behalte Content: View für diese Komponente bei: Ihre Ausgabe ist weiterhin für eine View-Hierarchie vorgesehen. Gehe nicht davon aus, dass die neuere Schreibweise in älteren SDKs verfügbar ist, nur weil die aktuelle Dokumentation ältere Deployment-Ziele aufführt. Siehe ContentBuilder.

Warum löst some View nicht alle Aspekte dieses Vertrags?

some View eignet sich für die body-Eigenschaften des Beispiels. Es verbirgt den konkreten Ergebnistyp und behält zugleich die Zusicherung bei, dass das Ergebnis eine View ist. Für die gespeicherte Closure wird stattdessen der benannte Parameter Content verwendet, damit die Komponente in ihrer Eigenschaft und ihrem Initializer auf denselben Ergebnistyp verweisen kann.

Swift 5.7 führte mit SE-0341 undurchsichtige Parameterdeklarationen ein. Ein direkter Parameter wie _ value: some View führt einen unbenannten generischen Parameter ein, dessen konkreter Typ vom Aufrufer bestimmt wird. Das bedeutet nicht, dass die Funktionsimplementierung eine beliebige View auswählen und überall dort übergeben kann, wo dieser unbenannte Typ erwartet wird. Siehe Opaque Parameter Declarations.

Insbesondere liegt ein Callback-Parameter der Form (some View) -> Content außerhalb der von SE-0341 dokumentierten unterstützten Positionen für undurchsichtige Parameter. Der Vorschlag verbietet undurchsichtige Typen ausdrücklich an der Eingabeposition des Funktionstyps eines Callbacks. Die Diskussion, die diese Syntax möglicherweise zulassen würde, steht unter den zukünftigen Erweiterungen und beschreibt kein implementiertes Verhalten. Die aktuelle Sprachreferenz zu Types enthält diese Einschränkung weiterhin.

Für diese Desktop-Aufgabe ist der unterstützte Weg einfach: Verwende für den von der Komponente bereitgestellten Wert einen konkreten Konfigurationstyp. Muss ein anderer wiederverwendbarer Container einen vom Aufrufer bereitgestellten Eingabetyp beibehalten, benenne diesen Typ mit einem generischen Parameter und verwende ihn durchgängig im gespeicherten Wert und im Callback. Keiner der beiden Entwürfe erfordert eine undurchsichtige Callback-Eingabe.

Was garantiert der Compiler tatsächlich?

Die Signatur garantiert eine aufrufbare Form: ein Argument vom Typ SwatchInput und ein Ergebnis, das Content: View erfüllt. Außerdem erhält der Closure-Parameter seinen abgeleiteten Typ, sodass der Aufrufer auf input.title, input.side und input.color zugreifen kann, ohne die Typannotation zu wiederholen. Swift dokumentiert diese Typableitung aus dem Closure-Kontext unter Closures.

Sie kann nicht garantieren, dass diese Eigenschaften die Anzeige beeinflussen. Beispielsweise ist der folgende Ausdruck innerhalb von ContentView.body ein gültiger Ersatz; er nimmt die Konfiguration bewusst entgegen, ignoriert sie aber:

SwatchPanel { _ in
    Text("Fixed preview")
}

Diese Grenze ist wichtig, wenn du die Anforderungen der Komponente formulierst. „Der Inhalt des Aufrufers nimmt SwatchInput entgegen“ ist ein Typvertrag. „Der Inhalt des Aufrufers zeigt die bereitgestellte Größe an“ ist eine Verhaltensanforderung. Wenn die Anzeige dieser Größe für die Komponente unerlässlich ist, gehört diese Darstellung in die Komponente, die nur einen kleineren Anpassungsbereich freigibt.

Nutze die folgenden Prüfungen zur Diagnose von Integrationsproblemen. Es handelt sich um Prüfschritte, die sich aus der Schnittstelle ergeben, nicht um berichtete Compiler-Testergebnisse:

Symptom Prüfung
Fehlendes Argument beim Aufruf von content Übergib die Konfiguration: content(input).
Diagnose zu einer escapenden Closure im Initializer Behalte @escaping bei, wenn du den Callback speicherst.
Mehrere benachbarte Views im Aufrufer verursachen Fehler Behalte die passende Builder-Annotation am Initializer-Parameter bei.
Eine explizit typisierte Closure wird abgelehnt Prüfe, ob ihre Eingabe SwatchInput statt eines nicht passenden Typs wie String ist.
Die Vorschau ignoriert den Größen-Schalter Prüfe, ob die Darstellung input.side ausliest.
Die Komponente zeigt weiterhin die anfängliche Konfiguration Prüfe, ob der Inhalt einmal erstellt und gespeichert wurde, statt den Callback mit der aktuellen Eingabe aufzurufen.

Halte den Vertrag so einfach, wie es die Aufgabe zulässt. Eine konkrete Konfiguration und ein generisches View-Ergebnis schaffen eine klare Schnittstelle für dieses Farbfeld-Panel: Die Komponente stellt aktuelle Werte bereit, der Aufrufer erhält sie mit vollständigen Typinformationen, und die gewählte Darstellung lässt sich einfach ersetzen.

Quellen