Ge en generisk SwiftUI-komponent ett standardutseende genom att lägga till en initializer som begränsas till en konkret innehållstyp. Behåll en separat builder-initializer för anpassning. Den här genomgången bygger en återanvändbar panel för ett macOS-fönster och förklarar när Swift 5.7:s inferens av standardargument kan förenkla gränssnittet.

Definiera de två anrop som komponenten ska stödja

Uppgiften är enkel: bygg en skrivbordspanel som ser komplett ut när den anropas med enbart en titel, samtidigt som en annan skärm kan tillhandahålla eget innehåll. Den omgivande titeln, mellanrummet och ramen ska vara konsekventa i båda fallen.

Det avsedda gränssnittet är DesktopPanel(title: "Workspace") för standardpresentationen och DesktopPanel(title: "Workspace") { Text("Three sample items") } för anpassat innehåll. Dessa anrop skapar olika specialiseringar av samma generiska komponent.

Swifts generiska parametrar står för konkreta typer. En begränsning som Content: View kräver konformans; ett krav på samma typ, till exempel Content == DefaultPanelContent, väljer en specifik typ. Språkhandboken beskriver både dessa krav och hur initializer-överlagringar matchas genom begränsningar. Generiska parametrar och argument

Det ger komponenten två tydliga ansvarsområden: dess body styr panelens gemensamma utseende, och dess initializer avgör vilket innehåll som placeras i den. Standardinnehållet får en namngiven vytyp så att den bekväma initializern kan hänvisa till en stabil och lättläst specialisering.

Bygg ett komplett macOS-exempel

Använd ett macOS-appmål med SwiftUI och ett distributionsmål på macOS 11 eller senare samt en kompilator för Swift 5.7 eller senare. Den lägsta macOS-versionen krävs för strukturen med App och WindowGroup nedan; Apple beskriver WindowGroup som scenbehållaren för ett fönsters vyhierarki. WindowGroup

Ersätt den genererade appens startpunkt med den här kompletta källfilen och ta bort den genererade ContentView.swift om den deklarerar en annan ContentView. Behåll exakt en @main-appdeklaration i målet. Alla värden som visas nedan är syntetiska lokala exempel.

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

Bygg och kör macOS-målet. Det förväntade resultatet är två paneler med samma yttre stil: den första innehåller standardtexten för arbetsytan och den andra innehåller en rad med ett antal följd av en layoutbeskrivning. Den här miljön saknar Swift-kompilator och Apple SDK, så exemplet granskades men kunde inte kompileras här.

Apples vägledning om anpassade vyer stöder den här sammansättningen: deklarera en typ som konformerar till View, implementera dess body och bygg ihop inbyggda och anpassade vyer i en hierarki. Den förklarar också att modifierare returnerar ändrade vyer, vilket är anledningen till att den gemensamma stilen ska omsluta panelens innehåll. Deklarera en anpassad vy

Varför den begränsade initializern löser den generiska typen

Den anpassade initializern tar emot en closure som returnerar Content. Anropet content() skapar värdet som lagras i panelen. Closuren används under initieringen, så implementationen lagrar den inte och behöver inte @escaping.

För anropet utan builder gör extensionen init(title:) tillgänglig specifikt när Content är DefaultPanelContent. Det kravet tillför den saknade typinformationen. Implementationen delegerar sedan till builder-initializern, så att egenskaperna initieras på ett enda ställe.

Standardvärdet får därmed typen DesktopPanel<DefaultPanelContent>. Innehållstypen för den anpassade panelen härleds från dess closure. Anropare behöver inte ange den potentiellt komplicerade typen.

En användbar följd är att en standardinstans redan är specialiserad. Du kan inte tilldela en panel med en annan innehållstyp till samma variabel bara för att båda värdena visar paneler. Ge den omgivande vyn en builder-baserad gren om gränssnittet behöver alternativa presentationer.

Builder-attributet gör den anpassade closuren med flera satser i exemplet möjlig. Det låter HStack och efterföljande Text ingå i det konstruerade innehållet; det ersätter inte komponentens eget krav Content: View. Apple beskriver hur buildern hanterar flera uttryck och villkorligt innehåll. ViewBuilder

När kan en enda builder-initializer med standardvärde ersätta överlagringen?

Swift 5.7 implementerade SE-0347, som gör att ett utelämnat argument kan härleda en generisk typ från sitt konkreta standarduttryck. För den här komponenten ger det ett kortare alternativ. SE-0347: Typinferens från standarduttryck

För en kompilator för Swift 5.7 eller senare ersätter du builder-initializern inuti DesktopPanel med följande fragment och tar bort hela den begränsade extensionen. Behåll importen, de lagrade egenskaperna, DefaultPanelContent, body och appkoden från det kompletta exemplet.

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

Om closuren utelämnas härleds nu Content från DefaultPanelContent(). Om en closure anges härleds typen i stället från det argumentet. Det här är inferens från standarduttryck, inte ett standardtypargument i komponentdeklarationen.

Förslaget begränsar den här inferensen: den berörda generiska parametern får bara förekomma på ett ställe i parameterlistan, och relaterade krav på samma typ måste uppfylla dess regler. Här förekommer Content bara i builder-parametern. Om den återanvänds i en annan initializer-parameter kan den här metoden sluta fungera. Detaljerad utformning av SE-0347

Välj överlagringen när standardinnehållet behöver indata från initializern. Exempelvis kan en standardundertitel som härleds från title skapas inuti init(title:) och skickas till builder-initializern. Ett standardargumentuttryck kan inte fånga det angivna title som om det vore en closure skapad inuti initializerns kropp.

Komponentkrav Lämpligt gränssnitt
Fast standardinnehåll och en kompilator för Swift 5.7+ En builder-parameter med standardvärde är koncis.
Standardinnehåll som skapas från andra indata till initializern Använd en begränsad initializer och skapa värdet inuti den.
Flera namngivna standardpresentationer Använd uttryckliga initializer-överlagringar med tydliga etiketter och begränsningar.
En uttrycklig lagrad typ för standardspecialiseringen Namnge DesktopPanel<DefaultPanelContent> eller inför ett typalias.

Försök inte uttrycka det här gränssnittet som struct DesktopPanel<Content: View = DefaultPanelContent>. Swifts aktuella grammatik för generiska parametrar tillhandahåller inte den syntaxen för standardtyp. De metoder som stöds här är begränsad initiering och inferens från standarduttryck. Generiska parametrar och argument

Bevara gränssnittet mellan olika builder-versioner

Apples aktuella dokumentation skiljer mellan Xcode 26 och tidigare versioner samt Xcode 27 och senare. Med den nyare verktygskedjan skapar ViewBuilder typoberoende innehåll; Apple introducerar ContentBuilder, ett typalias för ViewBuilder, som benämning på det här enhetliga beteendet. Skillnaden följer verktygskedjan som används för bygget, snarare än enbart appens distributionsmål. ViewBuilder, ContentBuilder

Exemplet behåller @ViewBuilder för kompatibilitet med äldre verktygskedjor. När du använder den nyare benämningen i Xcode 27 eller senare ändrar du parameterattributet till @ContentBuilder. Behåll Content: View: panelen kräver fortfarande innehåll som den kan placera i en vyhierarki.

Undvik att ange standardspecialiseringen som en nästlad typ för stack och tupel. Apples migreringsnot beskriver en ändring från TupleView till TupleContent i builder-funktioner med flera uttryck, som kan göra att begränsningar som namnger dessa genererade strukturer slutar fungera. Exemplet begränsar i stället extensionen till DefaultPanelContent; den interna layouten hålls bakom body: some View. TN3211: Lösa inkompatibiliteter i SwiftUI-källkod för State och ContentBuilder

Den här namngivna omslagsvyn gör även vanliga redigeringar enklare. Om du lägger till en tredje rad i standardinnehållet ändras dess implementation utan att du behöver skriva om en lång konkret typ i den bekväma initializern.

Diagnostisera kontraktet innan du ändrar layouten

Om komponenten inte kompilerar ska du först identifiera vilken del av gränssnittet som saknar typinformation. Att ändra utfyllnaden eller omsluta allt i ytterligare en stack löser inte en saknad generisk specialisering.

  1. Anropet utan builder kan inte härleda Content. Kontrollera att den begränsade init(title:) finns, eller att den initializern med standardvärde kompileras med Swift 5.7 eller senare.
  2. En konkret standardtyp kan inte tilldelas till Content. Kontrollera att tilldelningen sker inuti den begränsade extensionen. Konformans till View gör inte i sig DefaultPanelContent utbytbar mot alla möjliga Content-typer.
  3. Flera anpassade uttryck avvisas. Kontrollera att closure-parametern har builder-attributet.
  4. En uttrycklig typ avvisar anpassat innehåll. En annotering med DesktopPanel<DefaultPanelContent> låser med avsikt innehållstypen. Ta bort den annoteringen när anropet ska härleda en annan specialisering.
  5. En uppgradering av verktygskedjan gör att en tupelbaserad begränsning slutar fungera. Ersätt beroendet av den genererade builder-strukturen med en namngiven standardvy, eller följ Apples dokumenterade migrering för den specifika begränsningen.

Håll slutligen buildern lättviktig. I den här implementationen körs den varje gång panelen initieras; det är inget löfte om att den körs en gång. Apple påpekar att SwiftUI kan initiera vyer på nytt och rekommenderar att man undviker omfattande arbete i deras initializers. Deklarera en anpassad vy

Använd den begränsade initializern som uttrycklig utgångspunkt: den gör standardspecialiseringen synlig och gör det enkelt att härleda anpassat innehåll. Använd den enda initializern med standardvärde när dess kompilatorkrav och inferensbegränsningar passar ditt API. Båda metoderna låter macOS-panelen styra sitt gemensamma utseende samtidigt som anroparna bara tillhandahåller det innehåll de behöver.

Källor