Geef een generieke SwiftUI-component een standaarduiterlijk door een initializer toe te voegen die is beperkt tot een concreet inhoudstype. Behoud een afzonderlijke builder-initializer voor aanpassingen. In deze walkthrough bouwen we een herbruikbaar paneel voor een macOS-venster en leggen we uit wanneer de inferentie van standaardargumenten in Swift 5.7 de interface kan vereenvoudigen.

Definieer de twee aanroepen die je component moet ondersteunen

De taak van de lezer is eenvoudig: bouw een desktop-paneel dat er compleet uitziet wanneer het alleen met een titel wordt aangeroepen, maar waarmee een ander scherm eigen inhoud kan aanleveren. De omliggende titel, tussenruimte en rand moeten in beide gevallen hetzelfde blijven.

De beoogde interface is DesktopPanel(title: "Workspace") voor de standaardpresentatie en DesktopPanel(title: "Workspace") { Text("Three sample items") } voor aangepaste inhoud. Deze aanroepen maken verschillende specialisaties van dezelfde generieke component.

Generieke parameters in Swift staan voor concrete typen. Een constraint zoals Content: View vereist conformiteit; een same-type-eis zoals Content == DefaultPanelContent selecteert één specifiek type. De taalreferentie documenteert zowel deze eisen als de overloadresolutie van initializers via constraints. Generieke parameters en argumenten

Zo krijgt de component twee duidelijke verantwoordelijkheden: de body bepaalt de gedeelde vormgeving van het paneel, en de initializer bepaalt de inhoud die erin wordt geplaatst. De standaardinhoud krijgt een benoemd view-type, zodat de convenience-initializer naar een stabiele, leesbare specialisatie kan verwijzen.

Bouw een compleet macOS-voorbeeld

Gebruik voor deze walkthrough een macOS SwiftUI-appdoel met een deploymentdoel van macOS 11 of nieuwer en een compiler voor Swift 5.7 of nieuwer. De minimale macOS-versie is nodig voor de hieronder gebruikte structuur met App en WindowGroup; Apple documenteert WindowGroup als de scenecontainer voor de view-hiërarchie van een venster. WindowGroup

Vervang het gegenereerde ingangspunt van de app door dit volledige bronbestand en verwijder het gegenereerde ContentView.swift als daarin een andere ContentView wordt gedeclareerd. Behoud precies één @main-appdeclaratie in het doel. Alle hieronder getoonde waarden zijn synthetische lokale voorbeelden.

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

Bouw en start het macOS-doel. Het beoogde resultaat is twee panelen met dezelfde buitenste vormgeving: het eerste bevat de standaardtekst voor de werkruimte en het tweede bevat een rij met een telling, gevolgd door een beschrijving van de indeling. Deze omgeving heeft geen Swift-compiler of Apple SDK, dus het voorbeeld is nagekeken maar kon hier niet worden gecompileerd.

De richtlijnen van Apple voor aangepaste views ondersteunen deze compositie: declareer een conformer van View, implementeer de body en stel ingebouwde en aangepaste views samen in een hiërarchie. De richtlijnen leggen ook uit dat modifiers aangepaste views retourneren. Daarom hoort de gedeelde vormgeving rondom de inhoud van het paneel. Een aangepaste view declareren

Waarom de beperkte initializer het generieke type bepaalt

De aangepaste initializer ontvangt een closure die Content retourneert. Een aanroep van content() maakt de waarde die in het paneel wordt opgeslagen. De closure wordt tijdens de initialisatie gebruikt, dus deze implementatie slaat de closure niet op en vereist geen @escaping.

Voor de aanroep zonder builder maakt de extensie init(title:) beschikbaar, specifiek wanneer Content gelijk is aan DefaultPanelContent. Die eis levert de ontbrekende type-informatie. De implementatie delegeert vervolgens aan de builder-initializer, zodat de initialisatie van eigenschappen op één plek blijft.

De resulterende standaardwaarde heeft het type DesktopPanel<DefaultPanelContent>. Het inhoudstype van het aangepaste paneel wordt afgeleid uit de closure. Aanroepers hoeven dat mogelijk ingewikkelde type niet expliciet te vermelden.

Een nuttig gevolg is dat een standaardexemplaar al gespecialiseerd is. Je kunt niet zomaar een paneel met een ander inhoudstype aan dezelfde variabele toewijzen omdat beide waarden panelen weergeven. Geef de omringende view een builder-gebaseerde vertakking als de interface alternatieve presentaties nodig heeft.

Het builder-attribuut maakt de aangepaste closure met meerdere statements in het voorbeeld mogelijk. Het staat toe dat de HStack en de daaropvolgende Text deel uitmaken van de samengestelde inhoud; het vervangt niet de eigen eis Content: View van de component. Apple documenteert hoe de builder meerdere expressies en voorwaardelijke inhoud verwerkt. ViewBuilder

Wanneer kan één builder-initializer met standaardwaarde de overload vervangen?

Swift 5.7 implementeerde SE-0347, waarmee een concrete standaardexpressie van een weggelaten argument een generiek type kan afleiden. Voor deze component levert dat een korter alternatief op. SE-0347: Type-inferentie op basis van standaardexpressies

Vervang voor een compiler voor Swift 5.7 of nieuwer de builder-initializer binnen DesktopPanel door het volgende fragment en verwijder de volledige beperkte extensie. Behoud de import, opgeslagen eigenschappen, DefaultPanelContent, body en de app-code uit het complete voorbeeld.

// Replacement initializer inside DesktopPanel.
init(
    title: String,
    @ViewBuilder content: () -> Content = {
        DefaultPanelContent()
    }
) {
    self.title = title
    self.content = content()
}

Als je de closure weglaat, wordt Content nu afgeleid uit DefaultPanelContent(). Als je wel een closure opgeeft, wordt het type daarvan afgeleid. Dit is inferentie op basis van een standaardexpressie, geen standaardtypeargument in de declaratie van de component.

Het voorstel beperkt deze inferentie: de betreffende generieke parameter mag maar op één plek in de parameterlijst voorkomen en gerelateerde same-type-constraints moeten aan de regels voldoen. Hier komt Content alleen voor in de builderparameter. Als je het in een andere initializerparameter hergebruikt, kan deze aanpak ongeldig worden. Gedetailleerd ontwerp van SE-0347

Kies voor de overload als standaardinhoud invoer van de initializer nodig heeft. Een standaardondertitel die is afgeleid van title kan bijvoorbeeld binnen init(title:) worden gemaakt en aan de builder-initializer worden doorgegeven. Een standaardargumentexpressie kan de opgegeven title niet vastleggen alsof het een closure is die binnen de body van die initializer is gemaakt.

Vereiste van de component Geschikte interface
Vaste standaardinhoud en een compiler voor Swift 5.7 of nieuwer Een builderparameter met standaardwaarde is beknopt.
Standaardinhoud die uit andere invoer van de initializer wordt opgebouwd Gebruik een beperkte initializer en maak de waarde daarin aan.
Meerdere benoemde standaardpresentaties Gebruik expliciete initializer-overloads met duidelijke labels en constraints.
Een expliciet opgeslagen type voor de standaardspecialisatie Benoem DesktopPanel<DefaultPanelContent> of introduceer een typealias.

Probeer deze interface niet uit te drukken als struct DesktopPanel<Content: View = DefaultPanelContent>. De huidige grammatica voor generieke parameters in Swift biedt die syntaxis voor standaardtypen niet. De ondersteunde opties hier zijn initialisatie met constraints en inferentie op basis van standaardexpressies. Generieke parameters en argumenten

Behoud de interface bij verschillende versies van de builder

De huidige documentatie van Apple maakt onderscheid tussen Xcode 26 en ouder enerzijds en Xcode 27 en nieuwer anderzijds. Met de nieuwere toolchain bouwt ViewBuilder type-agnostische inhoud; Apple introduceert ContentBuilder, een typealias voor ViewBuilder, als schrijfwijze voor dit verenigde gedrag. Dit onderscheid hangt af van de buildtoolchain en niet alleen van het deploymentdoel van de app. ViewBuilder, ContentBuilder

Het voorbeeld behoudt @ViewBuilder voor compatibiliteit met oudere toolchains. Als je in Xcode 27 of nieuwer overstapt op de nieuwere schrijfwijze, wijzig je dat parameterattribuut in @ContentBuilder. Behoud Content: View: het paneel heeft nog steeds inhoud nodig die in een view-hiërarchie kan worden geplaatst.

Vermijd de schrijfwijze van de standaardspecialisatie als een genest stack-en-tuple-type. Apples migratienotitie documenteert een wijziging van TupleView naar TupleContent in builders met meerdere expressies, waardoor constraints die naar die gegenereerde structuren verwijzen, kunnen breken. Het voorbeeld beperkt de extensie in plaats daarvan tot DefaultPanelContent; de interne indeling blijft verborgen achter body: some View. TN3211: SwiftUI-broncode-incompatibiliteiten voor State en ContentBuilder oplossen

Deze benoemde wrapper maakt gewone wijzigingen ook eenvoudiger. Als je een derde regel aan de standaardinhoud toevoegt, verandert alleen de implementatie ervan; je hoeft geen lange concrete typevermelding in de convenience-initializer te herschrijven.

Stel de contractvoorwaarden vast voordat je de indeling wijzigt

Als de component niet compileert, bepaal dan eerst welk deel van de interface type-informatie mist. De padding wijzigen of alles in een andere stack wikkelen lost een ontbrekende generieke specialisatie niet op.

  1. De aanroep zonder builder kan Content niet afleiden. Controleer of de beperkte init(title:) bestaat, of dat de initializer met standaardwaarde wordt gecompileerd met Swift 5.7 of nieuwer.
  2. Een concrete standaardwaarde kan niet aan Content worden toegewezen. Controleer of de toewijzing binnen de beperkte extensie plaatsvindt. Alleen conformeren aan View maakt DefaultPanelContent niet uitwisselbaar met elk mogelijk Content-type.
  3. Meerdere aangepaste expressies worden afgewezen. Controleer of de closureparameter het builder-attribuut heeft.
  4. Een expliciet type wijst aangepaste inhoud af. Een annotatie als DesktopPanel<DefaultPanelContent> legt het inhoudstype bewust vast. Verwijder die annotatie als de aanroep een andere specialisatie moet afleiden.
  5. Een upgrade van de toolchain breekt een constraint op een tuple. Vervang de afhankelijkheid van de gegenereerde builderstructuur door een benoemde standaardview, of volg Apples gedocumenteerde migratie voor de specifieke constraint.

Houd de builder ten slotte licht. In deze implementatie wordt hij uitgevoerd wanneer het paneel wordt geïnitialiseerd; dit is geen garantie dat het werk maar één keer gebeurt. Apple merkt op dat SwiftUI views opnieuw kan initialiseren en raadt aan om betekenisvol werk in hun initializers te vermijden. Een aangepaste view declareren

Gebruik de beperkte initializer als expliciet uitgangspunt: daarmee wordt de standaardspecialisatie zichtbaar en blijft aangepaste inhoud eenvoudig af te leiden. Kies voor de enkele initializer met standaardwaarde als de compilervereiste en inferentiebeperkingen bij je API passen. Met beide benaderingen kan het macOS-paneel de gedeelde vormgeving beheren, terwijl aanroepers alleen de inhoud aanleveren die ze nodig hebben.

Bronnen