Declareer door de aanroeper aangeleverde inhoud als (Input) -> Content, waarbij Content beperkt is tot View. Zo kan een macOS-component een getypeerde waarde aanleveren, terwijl aanroepers de presentatie ervan kiezen. Het contract vereist dat die invoer wordt geaccepteerd; het kan niet vereisen dat deze wordt gebruikt.

Deze tutorial bouwt een kleine desktopvoorbeeldweergave voor een kleurstaal. De herbruikbare component beheert een formaatregeling en levert een synthetische configuratie aan de renderclosure van de aanroeper. De aanroeper tekent het kleurstaal zonder de interne status van de component te beheren.

Welke interface drukt deze vereiste uit?

Gebruik een benoemde generieke parameter voor de geretourneerde weergave en een concreet type voor de invoer van de component. In het onderstaande voorbeeld is de essentiële signatuur @ViewBuilder content: @escaping (SwatchInput) -> Content, binnen een declaratie met de beperking Content: View.

De invoer en uitvoer hebben verschillende functies. SwatchInput definieert de waarden die de component aanlevert. Content staat voor het concrete weergavetype dat door de closure van elke aanroeper wordt geproduceerd. Benoemde generieke parameters laten een declaratie deze typeverhoudingen uitdrukken en hergebruiken, zoals beschreven in het hoofdstuk Generics van Swift.

Apple gebruikt dit interfacepatroon in GeometryReader: de initializer accepteert een closure van het type (GeometryProxy) -> Content. De container levert de proxy aan en de aanroeper bouwt er inhoud mee. Dat is een nuttig precedent voor een aangepaste component die een eigen configuratie aanlevert. Zie Apples documentatie voor GeometryReader en init(content:).

Kies het contract op basis van de daadwerkelijke taak:

Vereiste Geschikte interface
De aanroeper levert inhoud aan zonder invoer van de component () -> Content
De component levert een specifieke configuratie aan (SwatchInput) -> Content
De container ontvangt een waarde waarvan het type per gebruik verschilt Een benoemde generieke parameter Input, met een overeenkomstige closure (Input) -> Content
De compiler moet bewijzen dat de aanroeper de configuratie gebruikt Een closuresignatuur drukt die vereiste niet uit

Deze keuzes volgen uit de functietypen van Swift: hun parametertypen en retourtype beschrijven wat kan worden doorgegeven en geretourneerd. Ze beschrijven niet hoe de implementatie de argumenten moet gebruiken. Zie het onderdeel Function Type in de taalreferentie van Swift.

Bouw een complete macOS-voorbeeldweergave voor een kleurstaal

Gebruik een macOS-appdoel met SwiftUI. Het voorbeeld gebruikt de gangbare schrijfwijze @ViewBuilder en is bedoeld voor Swift 5.7 of hoger, met een implementatiedoel van macOS 12 of hoger. Het vermijdt previewmacro's en nieuwere namen voor contentbuilders. Swift en de Apple SDK zijn niet beschikbaar in de onderzoeksomgeving, dus deze code is nagekeken maar hier niet gecompileerd of uitgevoerd.

Vervang in een SwiftUI-appproject in Xcode de gegenereerde appdeclaratie door het volgende complete programma. Verwijder de gegenereerde declaratie van ContentView als die hiermee zou worden gedupliceerd. Behoud precies één app-ingangspunt met de markering @main. Apples Xcode-help beschrijft hoe je begint met een SwiftUI-project of een SwiftUI-bestand toevoegt aan een bestaand project.

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()
        }
    }
}

De verwachte beginweergave bevat de schakelaar Large sample, de titel Sample A en een afgerond blauw vierkant met een zijde van 64 punten. Als je de schakelaar inschakelt, verandert de aangeleverde zijlengte in 120 punten en verschijnt Large preview. Deze formaten en labels zijn illustratieve lokale waarden, geen gemeten resultaten.

De component beheert isLarge. De berekende input bundelt de huidige selectie, en content(input) geeft die bundel door aan de aanroeper. SwiftUI beheert de statusopslag en werkt de delen van de weergavehiërarchie bij die afhankelijk zijn van een gewijzigde waarde, zoals beschreven onder State.

De aanroeper beheert de presentatie: een titel, een vorm en een optioneel bijschrift. De aanroeper kan een andere indeling kiezen met behoud van dezelfde getypeerde invoer. Zowel het paneel als de aanroepende weergave implementeren body om hun interfaces samen te stellen, volgens het door Apple beschreven model View.

Waarom zijn de builder- en escaping-annotaties nodig?

Met @ViewBuilder kan de aanroeper meerdere weergave-expressies en ondersteunde voorwaardelijke vertakkingen in één closure schrijven. In dit voorbeeld levert de aanroeper twee onvoorwaardelijke weergaven en één voorwaardelijk bijschrift aan. De builder combineert die inhoud tot een resultaat dat voldoet aan de beperking van de component voor de retourwaarde. Apple beschrijft meerdere onderliggende weergaven en voorwaardelijke constructies onder ViewBuilder.

De builder levert geen SwatchInput aan. De gewone closureparameter declareert de invoer, en content(input) levert het daadwerkelijke argument aan. Als de initializer () -> Content accepteerde, zou een builderannotatie die niet veranderen in een closure die een configuratie accepteert.

@escaping is nodig omdat de initializer de closure opslaat in een eigenschap om deze later aan te roepen vanuit body. In het hoofdstuk Closures legt Swift uit dat een closure escaping is wanneer deze kan worden aangeroepen nadat de ontvangende functie is teruggekeerd.

Voor een component waarvan de configuratie al definitief is bij initialisatie, is een ander ontwerp mogelijk: roep een niet-escaping builder direct aan en sla de resulterende Content-waarde op. Dat verandert het moment waarop de configuratie wordt toegepast. Hier maakt het bewaren van de closure de beoogde stroom expliciet: leid de huidige invoer af uit de status van de component en bouw vervolgens inhoud met die invoer.

Behoud de buildergrens van Xcode

De huidige documentatie van Apple maakt onderscheid tussen Xcode 26 en ouder en Xcode 27 en nieuwer. Voor de oudere tools beschrijft de documentatie ViewBuilder als een builder voor weergaven. Voor Xcode 27 en nieuwer beschrijft ze type-onafhankelijke constructie en beveelt ze de alias ContentBuilder aan om dat bredere gedrag aan te geven. Zie ViewBuilder.

Bij onderhoud aan een codebase met Xcode 27 of nieuwer kan de initializer in plaats daarvan @ContentBuilder gebruiken. Apple documenteert dit als een typealias voor ViewBuilder, waarbij conformiteit wordt afgedwongen via de contenttypen die het produceert. Behoud Content: View in deze component: de uitvoer is nog steeds bedoeld voor een weergavehiërarchie. Ga er niet van uit dat de nieuwere schrijfwijze in oudere SDK's bestaat, alleen omdat de huidige documentatie oudere implementatiedoelen vermeldt. Zie ContentBuilder.

Waarom lost some View niet elk onderdeel van dit contract op?

some View is geschikt voor de body-eigenschappen in het voorbeeld. Het verbergt het concrete resultaatsoort, maar behoudt de toezegging dat het resultaat een weergave is. De opgeslagen closure gebruikt in plaats daarvan de benoemde parameter Content, zodat de component naar hetzelfde resultaatsoort kan verwijzen in de eigenschap en de initializer.

Swift 5.7 introduceerde opaak-parameterdeclaraties via SE-0341. Een directe parameter zoals _ value: some View introduceert een niet-benoemde generieke parameter waarvan het concrete type door de aanroeper wordt bepaald. Dit betekent niet dat de functie-implementatie een willekeurige weergave kan kiezen en die kan doorgeven waar dat niet-benoemde type wordt verwacht. Zie Opaque Parameter Declarations.

Een callbackparameter geschreven als (some View) -> Content valt met name buiten de door SE-0341 gedocumenteerde ondersteunde posities voor opaak-parameters. Het voorstel verbiedt opaaktypen expliciet in de invoerpositie van het functietype van een callback. De bespreking van de mogelijkheid om deze syntaxis later toe te staan, staat onder toekomstige richtingen en beschrijft geen geïmplementeerd gedrag. De huidige referentie Types handhaaft deze beperking.

Voor deze desktoptaak is de ondersteunde aanpak eenvoudig: maak de door de component aangeleverde waarde een concreet configuratietype. Als een andere herbruikbare container een door de aanroeper aangeleverd invoertype moet behouden, geef dat type dan een naam met een generieke parameter en gebruik het consequent in de opgeslagen waarde en callback. Geen van beide ontwerpen vereist een opaak callbackinvoertype.

Wat garandeert de compiler daadwerkelijk?

De signatuur garandeert een aanroepbare vorm: één argument van het type SwatchInput en een resultaat dat voldoet aan Content: View. De signatuur geeft de closureparameter ook het afgeleide type, zodat de aanroeper toegang heeft tot input.title, input.side en input.color zonder een typeannotatie te herhalen. Swift documenteert deze afleiding uit de closurecontext in Closures.

De signatuur kan niet garanderen dat die eigenschappen de weergave beïnvloeden. Dit is bijvoorbeeld een geldige vervangende expressie in ContentView.body; deze accepteert de configuratie bewust, maar negeert haar:

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

Die grens is belangrijk bij het opstellen van de vereisten van de component. “De inhoud van de aanroeper accepteert SwatchInput” is een typecontract. “De inhoud van de aanroeper geeft de aangeleverde grootte weer” is een gedragsvereiste. Als het weergeven van die grootte essentieel is voor de component, plaats die weergave dan in de component en bied een kleiner aanpassingsgebied aan.

Gebruik de volgende controles om integratieproblemen op te sporen. Het zijn inspectiestappen die uit de interface zijn afgeleid, geen gerapporteerde resultaten van compilatietests:

Symptoom Controle
Ontbrekend argument bij het aanroepen van content Geef de configuratie door: content(input).
Diagnose over een escaping-closure in de initializer Behoud @escaping wanneer je de callback opslaat.
Meerdere naast elkaar staande weergaven veroorzaken fouten bij de aanroeper Behoud de toepasselijke builderannotatie op de parameter van de initializer.
Een expliciet getypeerde callback wordt afgewezen Controleer of de invoer SwatchInput is en niet een niet-verwant type zoals String.
De voorbeeldweergave negeert de grootte-instelling Controleer of de rendering input.side leest.
De component blijft de beginconfiguratie tonen Controleer of de inhoud één keer is gemaakt en opgeslagen, in plaats van de callback met de huidige invoer aan te roepen.

Houd het contract zo klein als de taak toelaat. Een concrete configuratie en een generiek resultaat voor de weergave geven dit kleurstaalpaneel een duidelijke grens: de component levert de huidige waarden aan, de aanroeper ontvangt ze met volledige type-informatie en de gekozen presentatie blijft eenvoudig te vervangen.

Bronnen