För en återanvändbar SwiftUI-panel i en Mac-app placerar du den huvudsakliga innehålls-buildern först, namnger de återstående builders och använder begränsade initialiserare för utelämnade områden. Det gör anropen lättlästa och ger frånvarande sidhuvuden och sidfötter explicita typer.

I den här handledningen bygger vi en liten komponent med områden för innehåll, sidhuvud och sidfot. Målet är att stödja fyra användbara anrop—endast innehåll, innehåll med valfritt tillbehör och innehåll med båda—utan att förlita sig på konkurrerande closures med standardvärden. Alla visade värden är syntetisk lokal text.

Vad gör en initialiserare med flera builders entydig?

Swifts syntax för flera avslutande closures utelämnar argumentetiketten för den första avslutande closure-argumentet och kräver etiketter för efterföljande avslutande closure-argument. Därför bör en komponent göra syftet med sin första builder tydligt genom komponentens namn och ge senare builders särskiljande etiketter. Syntaxen implementerades i Swift 5.3. SE-0279: Flera avslutande closures

För den här panelen är den fullständiga initialiseraren init(content:header:footer:). Vid anropsstället tillhandahåller det första blocket innehållet; header: och footer: identifierar de andra blocken. Deras deklarationsordning styr argumentordningen, medan komponentens body styr den visuella ordningen. Innehållet kan därför vara det första argumentet även om sidhuvudet visas ovanför det.

Apples aktuella Section-gränssnitt följer denna ordning med innehållet först. Den fullständiga initialiseraren tar emot tre builder-closures, och separata initialiserare stöder kombinationer där områden saknas. Det här är ett användbart gränssnittsexempel; den anpassade panelen nedan har sin egen layout och sitt eget beteende. Apple: Section, Apple: init(content:header:footer:)

En builder-attribut och en parameteretikett löser olika problem. Ett result-builder-attribut transformerar den tillhandahållna closure-kroppen; argumentetiketterna avgör hur anroparna identifierar parametrarna. Att lägga till @ViewBuilder återställer inte den första etiketten när syntaxen för avslutande closures utelämnar den. SE-0289: Result builders

Den skillnaden ger en praktisk designregel: undvik syskoninitialiserare där den enda användbara skillnaden försvinner när den första avslutande closure-argumentets etikett utelämnas. Två i övrigt likvärdiga initialiserare med namnen init(content:header:) och init(body:header:) skulle till exempel ha samma oetiketterade första block följt av header:. Det här är en designkonsekvens av reglerna för avslutande closures, inte ett skäl att lägga till fler builder-annoteringar.

Vilka gränser för kompilatorn och SwiftUI är viktiga?

Håll tre detaljer i åtanke: kompilatorn, det valda Swift-språkläget och SwiftUI-SDK:t. Stöd för flera avslutande closures kom i Swift 5.3, medan den formella result-builder-funktionen implementerades i Swift 5.4. Dessa milstolpar beskriver språkstödet; de fastställer inte att varje initialiserare i ett ramverk är tillgänglig. SE-0279, SE-0289

Apples aktuella dokumentation anger också en gräns för verktygskedjan: Xcode 26 och tidigare använder det etablerade beteendet för att bygga vyer, medan Xcode 27 och senare skapar innehållsoberoende typer från dessa builders. För det nyare beteendet hänvisar Apple utvecklare till stavningen @ContentBuilder. ContentBuilder är ett typalias för ViewBuilder; begränsningarna i det omslutande API:t avgör fortfarande vilket innehåll som godtas. Apple: ViewBuilder, Apple: ContentBuilder

Det fullständiga exemplet nedan använder stavningen @ViewBuilder som baslinje för Xcode 26 och har macOS 11 eller senare som mål. När du använder den dokumenterade gränssnittsstilen för Xcode 27 ersätter du annoteringarna för initialiserarens parametrar med @ContentBuilder. Behåll de tre generiska View-begränsningarna: den här komponenten placerar alla tre områdena i en vylayout.

Undvik att namnge den tupeltyp som en builder genererar i komponentens kontrakt. Apple dokumenterar källkodsinkompatibiliteter som rör explicita TupleView-begränsningar när man använder den nyare builder-implementationen. Generiska områdestyper gör att den här panelen slipper beroendet. Apple: TN3211, Lösa källkodsinkompatibiliteter i SwiftUI för State och ContentBuilder

Bygg en panel med fyra explicita initialiserarformer

Använd en fristående generisk typ för varje område. Om du återanvänder en generisk parameter måste områdena ha samma konkreta typ, vilket är en onödig begränsning när det ena området innehåller en enda textvy och ett annat innehåller flera vyer.

För ett frånvarande område specialiserar du dess generiska parameter till EmptyView. Apple beskriver just den här användningen av EmptyView: att representera ett frånvarande underordnat element i en generisk vytyp. De begränsade initialiserarna nedan gör det valet explicit i stället för att be anroparna härleda en ospecificerad tillbehörstyp. Apple: EmptyView

De två Swift-blocken i det här avsnittet utgör en fullständig programfil. I ett SwiftUI-appmål för macOS ersätter du de genererade app- och innehållsvydeklarationerna med dessa block och behåller exakt en @main-deklaration. Det första blocket definierar komponenten och alla fyra initialiserarformer.

import SwiftUI

struct BuilderPanel<Content: View, Header: View, Footer: View>: View {
    private let content: Content
    private let header: Header
    private let footer: Footer

    init(
        @ViewBuilder content: () -> Content,
        @ViewBuilder header: () -> Header,
        @ViewBuilder footer: () -> Footer
    ) {
        self.content = content()
        self.header = header()
        self.footer = footer()
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            header
            content
            footer
        }
        .padding()
    }
}

extension BuilderPanel where Footer == EmptyView {
    init(
        @ViewBuilder content: () -> Content,
        @ViewBuilder header: () -> Header
    ) {
        self.init(
            content: content,
            header: header,
            footer: { EmptyView() }
        )
    }
}

extension BuilderPanel where Header == EmptyView {
    init(
        @ViewBuilder content: () -> Content,
        @ViewBuilder footer: () -> Footer
    ) {
        self.init(
            content: content,
            header: { EmptyView() },
            footer: footer
        )
    }
}

extension BuilderPanel where Header == EmptyView, Footer == EmptyView {
    init(@ViewBuilder content: () -> Content) {
        self.init(
            content: content,
            header: { EmptyView() },
            footer: { EmptyView() }
        )
    }
}

Den fullständiga initialiseraren anropar varje builder en gång under initialiseringen av det panelvärdet och lagrar de returnerade värdena. Hjälpinitialiserarna delegerar till den. Tidpunkten är en egenskap hos den här implementationen: den utlovar inte att en builder bara körs en gång under gränssnittets livstid, eftersom en ny panelinstans anropar dess initialiserare igen.

Lägg till det här andra blocket i samma fil. Det provar alla stödda former, inklusive flera instruktioner i en builder.

struct PanelDemo: View {
    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 16) {
                BuilderPanel {
                    Text("Canvas A")
                }

                BuilderPanel {
                    Text("Canvas B")
                    Text("Grid: 12 columns")
                } header: {
                    Text("Layout")
                }

                BuilderPanel {
                    Text("Canvas C")
                } footer: {
                    Text("Units: points")
                }

                BuilderPanel {
                    Text("Canvas D")
                } header: {
                    Text("Preview")
                } footer: {
                    Text("Scale: 100 percent")
                }
            }
            .padding()
        }
        .frame(minWidth: 360, minHeight: 440)
    }
}

@main
struct BuilderPanelDemoApp: App {
    var body: some Scene {
        WindowGroup {
            PanelDemo()
        }
    }
}

Bygg och kör appen i Xcode för att verifiera kontraktet mot det valda SDK:t. Koden granskades, men kompilerades eller kördes inte här; i undersökningsmiljön fanns varken en Swift-kompilator eller macOS SwiftUI-SDK:t.

Matcha varje anrop mot ett konkret typkontrakt

En tabell över anropsformerna är ett användbart beslutsstöd. Den anger vilken initialiserare som bör väljas och vilka konkreta typer som tilldelas saknade områden. Det här är förväntningar som härletts från exempeldeklarationerna och som du kan kontrollera i ditt projekt.

Angivna områden Initialiserarens etiketter Begränsningar för frånvarande områden
Endast innehåll content: Header == EmptyView, Footer == EmptyView
Innehåll och sidhuvud content:header: Footer == EmptyView
Innehåll och sidfot content:footer: Header == EmptyView
Alla tre content:header:footer: Inga

Varje kortare initialiserare kräver alla closures som den deklarerar. Inget standardargument gör att initialiseraren med tre builders konkurrerar om anropet med endast innehåll. Tillbehörsformerna med bara sidhuvud respektive bara sidfot skiljs åt av sina andra etiketter, även när båda closures returnerar Text.

Använd tabellen när du ändrar komponenten: om en ny initialiserare godtar en befintlig anropsform kontrollerar du om den nya skillnaden fortfarande syns när etiketten för den första avslutande closure-argumentet försvinner. Det fångar överlappningar i gränssnittet innan de leder till svårtolkade felmeddelanden i en större vy.

Varför undvika flera builder-closures med standardvärden?

Standardargument är användbara, men flera closure-parametrar med standardvärden introducerar matchningsregler som beror på Swifts språkläge. SE-0286 beskriver framåtriktad matchning, en kompatibilitetsheuristik för att hoppa över parametrar med standardvärden, och reservlösningen med bakåtriktad matchning i äldre lägen. Den beskriver också flaggan för den kommande funktionen ForwardTrailingClosures, som implementerades i Swift 5.8. SE-0286: Framåtsökning vid matchning av avslutande closures

Den praktiska rekommendationen här är medvetet snävare än de reglerna: en liten återanvändbar panel kan undvika att förlita sig på dem. Kräv den huvudsakliga buildern och uttryck stödda utelämningar genom begränsade initialiserare. Då syns de stödda kombinationerna i deklarationerna, och avsikten med komponenten blir inte beroende av matchningsbeteende som varierar mellan kompilatorversioner.

Ett utelämnat tillbehör och en tom tillbehörsclosure är också två olika gränssnittsval. Initialiseraren för endast innehåll väljer uttryckligen EmptyView för båda tillbehören. Om du tillhandahåller en tom builder får SDK:ts builder-implementation avgöra resultatet av closuren. För den här komponenten utelämnar du oanvända områden och låter hjälpinitialiseraren tillhandahålla frånvarotypen som dokumenterats.

Det finns en gräns för hur långt det här tillvägagångssättet räcker. Tre områden ger fyra användbara kombinationer när innehåll alltid krävs. En komponent med många oberoende valfria områden skulle kräva en mycket större familj av överlagringar. Innan du lägger till sådana överlagringar bör du avgöra vilka kombinationer läsarna faktiskt behöver; en mindre uppsättning meningsfulla komponentvarianter kan vara enklare att använda och underhålla.

Hur felsöker du ett tvetydigt anrop?

Börja med det minsta panelanrop som återskapar problemet. Följ sedan den här ordningen; den skiljer argumentmatchning från fel inne i en builder.

  1. Skriv alla closure-argument inom parenteser. Ange content:, header: och footer: i deklarationsordning. Om det löser anropet undersöker du vilken skillnad syntaxen för avslutande closures tog bort.
  2. Ersätt tillfälligt varje område med ett enda Text-uttryck. Om anropet då fungerar återställer du de ursprungliga uttrycken ett i taget för att hitta den builder-kropp som orsakar felet.
  3. Jämför initialiserarformerna. Leta efter konkurrerande standardvärden, omvänd ordning på closures och överlagringar som bara skiljer sig åt genom etiketten på den första buildern.
  4. Kontrollera begränsningarna för frånvarande områden. Varje utelämnat generiskt område bör ha en avsedd konkret typ i den valda initialiseraren.
  5. Kontrollera verktygskedjans gräns. Efter uppgradering till Xcode 27 undersöker du explicita builder-resultatbegränsningar och kolliderande typnamn innan du gör om initialiserarfamiljen.

Om du skriver etiketterna inom parenteser bevaras argumentnamnen som den första avslutande closuren utelämnar. Swift stöder båda formerna, så den fullt etiketterade formen är användbar vid felsökning och passar som slutlig syntax när den gör ett visst anrop lättare att läsa. The Swift Programming Language: Closures

För felmeddelanden från nyare SDK:er rekommenderar Apple särskilt att kvalificera typer som kolliderar mellan moduler och uppdatera begränsningar som är beroende av äldre tuppeltyper som utdata. Dessa problem kan uppstå inuti ett i övrigt välutformat initialiseraranrop; fler överlagringar löser inte det underliggande problemet. Apple: TN3211

Ett tydligt initialiserarkontrakt börjar med de anrop som komponenten måste stödja. Placera det primära innehållet först, ge tillbehörsbuilders meningsfulla etiketter och tilldela explicita typer till frånvarande områden. Bygg sedan varje stödd anropsform med den kompilator, det språkläge och det SDK du har valt innan du utökar gränssnittet.

Källor