För att bygga en generisk SwiftUI-komponent, kräv dess initialiserare i ett protokoll eller injicera en closure som returnerar den. Denna handledning använder båda tillvägagångssätten för att visa samma syntetiska objekt i ett macOS-fönster, med explicita kompilator- och SDK-gränser.

Vad måste en generisk komponent lovsa?

Anta att en macOS-app visar ett litet objektkort. Du vill ha en återanvändbar panel för att bygga kortet från ett objekt, samtidigt som anroparna får välja presentationen. Den centrala frågan är vem som äger konstruktionen: komponentens deklarerade gränssnitt, eller anroparen som levererar komponenten.

Ett generiskt krav ger implementationen tillgång till de kapacitet som anges av kravet. Att skriva Component: View fastställer att komponenten är en SwiftUI-vy; det fastställer inte att komponenten har en initialiserare med namnet init(item:). Swifts Generics-kapitel förklarar protokollbegränsningar, medan Apples View-dokumentation beskriver vyens krävda body-egenskap.

Följande fragment är därmed avsiktligt ogiltigt:

// Invalid fragment: View does not require init(item:).
@MainActor
func construct<Component: View>(item: Item) -> Component {
    Component(item: item)
}

Att lägga till den initialiseraren i en konkret komponent gör inte att den blir tillgänglig genom varje generisk parameter som bara är begränsad till View. Det återanvändbara API:et behöver ett explicit konstruktionskontrakt. Använd denna beslutstabell innan du ändrar komponenten:

Konstruktionskrav Lämpligt gränssnitt Orsak
Varje deltagande komponent accepterar samma objekt Protokoll som kräver init(item:) Konstruktionen tillhör komponentfamiljens gränssnitt.
Anroparna väljer ytterligare konfiguration eller modifierare Fabrik-closure som returnerar den generiska komponenten Konstruktionen stannar på anropsplatsen.
Anroparen har redan en färdig komponent Lagra det generiska komponentvärdet Konstruktionen har redan skett.
Presentationen växlar bland en ändrad uppsättning layouter Komposera alternativen inuti en SwiftUI-vy Omgivande API kan behålla en konkret vystruktur.

Denna tabell är ett designstöd härlett från de dokumenterade språkreglerna. De två första alternativen implementeras tillsammans nedan så att deras beteende kan jämföras direkt.

Bygg ett komplett macOS-exempel

Använd ett Xcode macOS-apptarget med SwiftUI och en macOS-distributionsmålversion på 11 eller senare. Exemplet är skrivet för en Swift 6-kompilator med matchande Apple SDK. Dess explicita @MainActor-annoteringar kräver Swift 5.5 eller senare, så listningen är inte avsedd för tidigare kompilatorer. Apple dokumenterar WindowGroup som tillgänglig från macOS 11 och som en container för en apps vyhierarki. Se WindowGroup.

Ersätt den genererade appstartfilen med följande kompletta program. Se till att targeten innehåller endast en @main-appdeklaration. Den genererade ContentView-filen kan ligga oanvänd. Inga paket, externa resurser eller nedladdade exempeldata behövs.

import SwiftUI

struct Item {
    let title: String
    let detail: String
}

@MainActor
protocol ItemComponent: View {
    init(item: Item)
}

@MainActor
struct ItemCard: ItemComponent {
    let item: Item

    init(item: Item) {
        self.item = item
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 6) {
            Text(item.title)
                .font(.headline)
            Text(item.detail)
        }
        .padding()
        .background(Color.gray.opacity(0.12))
        .cornerRadius(8)
    }
}

@MainActor
struct ContractPanel<Component: ItemComponent>: View {
    let item: Item

    init(item: Item, component: Component.Type) {
        self.item = item
    }

    var body: some View {
        Component(item: item)
    }
}

@MainActor
struct FactoryPanel<Component: View>: View {
    private let makeComponent: @MainActor () -> Component

    init(make: @escaping @MainActor () -> Component) {
        self.makeComponent = make
    }

    var body: some View {
        makeComponent()
    }
}

@MainActor
struct DemoView: View {
    private let item = Item(
        title: "Sample tile",
        detail: "Blue circle on a neutral background"
    )

    var body: some View {
        VStack(alignment: .leading, spacing: 18) {
            Text("Initializer contract")
            ContractPanel(item: item, component: ItemCard.self)

            Text("Injected factory")
            FactoryPanel {
                ItemCard(item: item)
                    .padding(.leading, 12)
            }

            Text("Alternative factory output")
            FactoryPanel {
                Text(item.title + ": " + item.detail)
            }
        }
        .padding(24)
        .frame(width: 420)
    }
}

@main
@MainActor
struct ComponentDemoApp: App {
    var body: some Scene {
        WindowGroup {
            DemoView()
        }
    }
}

Kör macOS-apptargeten. Det avsedda resultatet är ett fönster som innehåller två kortpresentationer och en enkel textpresentation. Det andra kortet har extra vänsterpadding som levereras av dess anropar. Dessa är syntetiska exempel på API-beteende, inte mätta resultat. Ingen Swift-kompilator är installerad i forskningsmiljön, så denna listning har granskats men inte kompilerats eller körts här.

Huvudaktörsannoteringarna gör UI-konstruktionsgränsen explicit. Apples nuvarande View-deklaration är annoterad med @MainActor och @preconcurrency; dess dokumentation förklarar även hur isolering ärvs när överensstämmelse förekommer i en typs ursprungliga deklaration. Se View.

Hur fungerar initialiserarkontraktet?

ItemComponent förfinar View och lägger till exakt ett konstruktionskrav: init(item:). Swift stöder initialiserarkrav direkt i protokoll. En överensstämmande typ levererar implementationen; protokollet anger signaturen utan en initialiserarkropp. Se Protokoll: Initialiserarkrav.

Det kravet gör att Component(item: item) blir giltigt inuti ContractPanel. Argumentet component: Component.Type levererar en konkret typ till generisk inferens: att skicka ItemCard.self bestämmer att denna panels komponenttyp är ItemCard. Panelen behöver inte lagra metatypen eftersom dess body konstruerar genom den generiska typparametern.

Detta kontrakt är användbart när varje komponent ska kunna konstrueras från samma indata. Till exempel kunde en appägd familj av kompakterade och expanderade objektpresentationer dela kravet. En anropare kan då byta den valda komponenttypen utan att skriva om panelens konstruktionsuttryck.

Kompromissen visas när en implementation behöver extra konfiguration. Att kräva init(item:) innebär att varje överensstämmande komponent måste erbjuda den konstruktionsvägen, även om dess föredragna initialiserare också accepterar en rubrik eller layoutinställning. Om den extra informationen tillhör anroparen uttrycker fabriktillvägagångssättet nedan oftast kravet mer direkt.

För klassbaserade implementationer måste en initialiserare som uppfyller ett protokollkrav vara required om inte klassen är final. En underklass som också åsidosätter en matchande superklassdesignerad initialiserare kan behöva både required och override. Structs som ItemCard använder inte required. Dessa skillnader dokumenteras i Protokoll.

När ska konstruktionen flytta in i en fabrik?

FactoryPanel accepterar en funktion av typen @MainActor () -> Component. Dess löfte är bara att anropa funktionen producerar den valda komponenttypen. Closuren kan anropa en initialiserare, fånga konfiguration, eller returnera en vy med modifierare applicerade.

Programmet demonstrerar den praktiska skillnaden: fabriken kan returnera ett paddat ItemCard, eller den kan returnera Text, utan att kräva att någon av de resulterande typerna adopterar ItemComponent. Apple förklarar att vymodifierare kan wrappa sin indata i en annan vy, vilket är anledningen till att låta kompilatorn inferera fabrikens returtyp är användbart här. Se View.

Initialiseraren markerar closuren @escaping eftersom panelen lagrar den för senare anrop. Swifts Closures-kapitel dokumenterar escapande closures och deras förmåga att fånga omgivande värden. I det här exemplet fångar closuren det syntetiska objekt som används av den omgivande vyn.

Välj fabrikens indata medvetet. En nollargumentfabrik innebär att anroparen fångar allt som behövs för konstruktion. Om panelen själv väljer ett objekt kan en parameteriserad fabrik uttrycka det ansvaret tydligare. Följande är en ersättningskomponentdefinition att använda tillsammans med den befintliga Item; det är inte en andra komplett app:

@MainActor
struct ItemFactoryPanel<Component: View>: View {
    let item: Item
    private let makeComponent: @MainActor (Item) -> Component

    init(
        item: Item,
        make: @escaping @MainActor (Item) -> Component
    ) {
        self.item = item
        self.makeComponent = make
    }

    var body: some View {
        makeComponent(item)
    }
}

Med den definitionen levererar panelen objektet och anroparen levererar presentationen. Håll fabrikarbetet begränsat till att producera vyvärdet: exempelkonstruktionskontraktet lovar inte engångskörning, persistent lagring eller cachelagring.

Behöver fabriker en result builder?

Fabrikerna i det kompletta programmet är vanliga closures. Var och en returnerar ett uttryck, så ingen anpassad builder-annotering behövs. Om flera child-vyer krävs räcker att returnera en enda VStack för detta API; stacken innehåller child-vyerna.

En result builder blir användbar när fabrikparametern själv ska acceptera en deklarativ sekvens av uttryck eller villkorligt innehåll. Swift Evolution registrerar den standardiserade result-builder-funktionen som implementerad i Swift 5.4. Se SE-0289: Result builders.

Behåll SDK-gränsen när du antar builder-syntax. Apples nuvarande dokumentation beskriver @ViewBuilder för vyproducerande parametrar i Xcode 26 och tidigare. För Xcode 27 och senare beskriver den typagnostisk konstruktion och rekommenderar ContentBuilder-aliaset för att indikera det beteendet. Se ViewBuilder och ContentBuilder. Den vanliga closuren som används här undviker att kräva det nyare aliaset.

Varför ersätter inte opaka eller existentiella typer kontraktet?

some View döljer en specifik underliggande resultattyp samtidigt som dess identitet bevaras. Det fungerar bra för body-egenskaperna i detta program. Det lägger inte till init(item:) till View-gränssnittet. ett inboxat värde skrivet som any View döljer sin underliggande typ annorlunda och bevarar inte samma statiska typidentitet. Se Opaque and Boxed Protocol Types.

Gör inte detta till ett generellt påstående att existentiella värden aldrig kan skickas till generiska funktioner. SE-0352 införde implicit öppnade existentiella i Swift 5.7, vilket tillåter behöriga anrop att binda en generisk parameter till en existentiells underliggande typ. Det dokumenterar också restriktioner, inklusive resultat som bäddar in den öppnade typen i ett generiskt wrapper där resultatet inte kan representeras efter utplånning. Se Implicitly Opened Existentials.

För denna paneluppgift är det tillräckligt att behålla den konkreta komponenttypen genom en generisk parameter. Det finns inget behov att införa existentiell lagring bara för att låta anroparna välja hur man konstruerar en vy.

Hur kan du diagnostisera ett constructionsfel?

Följ det uttryck som misslyckas och inspektera det kontrakt som finns tillgänglig på den platsen:

  1. Om Component(item:) misslyckas, kontrollera att det generiska kravet innehåller ett protokoll som kräver den exakta initialiserarsignaturen.
  2. Om kompilatorn inte kan inferera Component, leverera en konkret metatyp som ItemCard.self, eller en fabrik med ett konkret returuttryck.
  3. Om en komponent behöver anroparspecifik konfiguration, flytta konstruktionen in i fabriken istället för att lägga till orelaterade krav till protokollet.
  4. Om lagring av en closure utlöser ett escaping-closure-diagnostiskt meddelande, kontrollera initialiserarens @escaping-annotering.
  5. Om konstruktion utlöser ett actor-isolationsdiagnostiskt meddelande, kontrollera att både anropsplatsen och fabrikens funktionsuttrycker den avsedda huvudaktörsgränsen.
  6. Om en existentiell orsakar ett generiskt-resultatfel, inspektera resultattyprestriktionerna i SE-0352 innan du antar att en kompilatoruppgradering tar bort begränsningen.

För en delad macOS-komponentfamilj, kräv initialiseraren när enhetlig konstruktion är del av gränssnittet. Använd en injicerad fabrik när anroparen ska sammanställa och konfigurera presentationen. Båda vägarna gör det saknade konstruktionslovet explicit samtidigt som de bevarar den konkreta komponenttyp som panelen behöver.

Källor