Gör din SwiftUI-komponent generisk över protokollets konformerande typ, och deklarera sedan dess lagrade vy som Provider.Content. Detta bevarar relationen mellan providern och dess associerade vitype genom hela komponenten.

Den här handledningen bygger ett litet macOS-fönster som innehåller två syntetiska paneler. Uppgiften är att lagra en provider och vyn den producerar utan att ersätta den associerade typen med any View eller AnyView. Samma mönster gäller när en återanvändbar skrivbords-komponent accepterar en anpassad etikett, tillbehör eller innehållsprovider.

Vilken relation måste de lagrade egenskaperna bevara?

En associerad typ namnger en typ som väljs av varje protokollkonformens. Om ett protokoll deklarerar associatedtype Content: View, måste en konformerande provider leverera en specifik innehållstyp. Generisk kod kan referera till den typen som Provider.Content, även när dess implementation är dold bakom ett opakt resultat. Swift dokumenterar associerade typer och generiska begränsningar i sitt Generics-kapitel.

Komponenten behöver alltså två kopplade deklarationer: en generisk parameter begränsad till provider-protokollet, och en lagrad innehållsegenskap vars typ är den parameterns associerade typ. Att deklarera två oberoende egenskaper som bara konformerar till protokoll uttrycker inte denna koppling.

SwiftUI använder ett relaterat kontrakt själv: View deklarerar associatedtype Body: View, och Swift härleder den associerade typen från en anpassad vys body-implementation. Din providers Content och komponentens Body är separata associerade typer; de behöver inte vara lika. Se Apples Body-dokumentation.

Bygg ett komplett macOS-exempel

Använd ett Xcode macOS-app-projekt med SwiftUI och en macOS 13 eller senare distributionsmål för det här exemplet. Ersätt den genererade app- och content-view-deklarationen med följande enda fil, med exakt en @main-deklaration i targetet. Apple dokumenterar denna app-entrystruktur och användningen av WindowGroup i App.

Exemplet använder avsiktligt den etablerade associerade-typ- och namngivna-generiska syntaxen. Opaka resultat implementerades i Swift 5.1; den valfria primära-associerade-typ-syntaxen som diskuteras senare kräver Swift 5.7. Den kompletta filen är avsedd för en nuvarande Swift 6-toolchain. Den granskades mot den hämtade dokumentationen men kompilerades inte här eftersom den här miljön saknar Swift-kompilator. Det valda macOS-distributionsmålet är ett exempel på förutsättning, inte ett påstående om den tidigaste OS som stöder varje underliggande funktion. Se SE-0244 och SE-0346.

import SwiftUI

@MainActor
protocol TileProvider {
    associatedtype Content: View

    var title: String { get }
    func makeContent() -> Content
}

@MainActor
struct TextTile: TileProvider {
    let title: String
    let message: String

    func makeContent() -> Text {
        Text(message)
    }
}

@MainActor
struct CircleTile: TileProvider {
    let title: String
    let caption: String

    func makeContent() -> some View {
        HStack(spacing: 8) {
            Circle()
                .fill(Color.blue)
                .frame(width: 16, height: 16)
            Text(caption)
        }
    }
}

@MainActor
struct TileCard<Provider: TileProvider>: View {
    private let provider: Provider
    private let content: Provider.Content

    init(provider: Provider) {
        self.provider = provider
        self.content = provider.makeContent()
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(provider.title)
                .font(.headline)
            content
        }
        .padding()
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(Color.gray.opacity(0.12))
    }
}

@MainActor
struct ContentView: View {
    var body: some View {
        VStack(spacing: 12) {
            TileCard(provider: TextTile(
                title: "Text tile",
                message: "Blue square"
            ))
            TileCard(provider: CircleTile(
                title: "Shape tile",
                caption: "Blue circle"
            ))
        }
        .padding()
        .frame(width: 360)
    }
}

@main
@MainActor
struct AssociatedViewDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Bygg och kör macOS-targetet. Det avsedda resultatet är ett fönster som innehåller en textpanel och en panel med en blå cirkel bredvid sin rubrik. Båda använder samma generiska komponent, med provider-typen härledd oberoende vid varje anropsplats.

De explicita @MainActor-annoteringarna håller dessa UI-producerande deklarationer i samma aktörskontext. Apples nuvarande View-dokumentation deklarerar protokollet med main-actor-isolering. Annoteringarna här gör också det anpassade provider-protokollets avsedda kontext explicita.

Varför bevarar den generiska komponenten typen?

För den första panelen är Provider TextTile, så Provider.Content är Text. För den andra panelen är Provider CircleTile, och dess associerade innehållstyp är det opaka resultatet av CircleTile.makeContent(). Du kan namngiva den typen genom CircleTile.Content utan att skriva ut den nästlade SwiftUI-layout-typen.

Den avgörande tilldelningen är self.content = provider.makeContent(). Protokollet garanterar att den här metoden returnerar exakt Provider.Content, vilket är exakt den lagrade egenskapens deklarerade typ. Detta är en kompileringstidsrelation härledd från den generiska begränsningen, snarare än en körtidsjämförelse av typer. Swifts Generics-kapitel förklarar åtkomst till associerade typer genom begränsade generiska parametrar.

Att använda some View i den konformerande implementationen radera inte denna relation. Ett opakt resultat doljer den konkreta implementationen medan det bevarar dess typidentitet. Det gör det lämpligt som ett vittne för associerad typ. Swift förklarar skillnaden i Opaque and Boxed Protocol Types.

Dessa paneler har olika specialiseringar: TileCard<TextTile> och TileCard<CircleTile>. Återanvändning av källdeklarationen kräver inte att varje panel har en universell lagrad innehållstyp.

Varför löser inte oberoende opaka eller existentiella egenskaper det?

Följande är diagnostiska fragment, inte ersättningar för det completa programmet:

// Diagnostic fragment: no initializer establishes the opaque type.
let content: some View

// Diagnostic fragment: hides which conforming provider is stored.
let provider: any TileProvider

// Diagnostic fragment: stores an existential view value.
let content: any View

En opak lagrad egenskap behöver en deklarationsinitialiserare som etablerar dess underliggande typ. Att skriva some View skapar ingen anropsvald generisk parameter som en vilken komponentinitialiserare som helst kan fylla i senare. Till exempel etablerar let content: some View = Text("Sample") en typ för den declarationen; det kopplar inte egenskapen till en annan egenskaps associerade typ. Den här lagrade-egenskapsregeln dokumenteras i SE-0244: Opaque Result Types.

En existenziell som any TileProvider tillåter lagring av olika konformerande provider-typer. Den doljer den specifika konformerande typen, så deklarationen exponerar inte längre relationen som Provider.Content. På samma sätt är any View en existenziell container; det är inte samma kontrakt som en namngiven generisk typ som konformerar till View. Se Swifts förklaring av boxade protokolltyper.

Swift kan implicit öppna existentiella argument för vissa generiska anrop. Det gör inte att varje existenziell är användbar i varje generisk initialiserare. Särskilt begränsar SE-0352 öppning när den okända underliggande typen skulle fly undan inuti ett generiskt resultat som X<T>. En generisk komponent-specialisering kan präcis stöta på den gränsen. Håll providern generisk innan du konstruerar panelen istället för att anta att en existenziell alltid kan konverteras tillbaka till en beständig specialisering. Se SE-0352: Implicitly Opened Existentials.

Välj lagring utifrån när innehåll ska produceras

Exemplet anropar makeContent() vid varje TileCard-initialisering och lagrar den returnerade vy-värdet. Det etablerar inte en beständig cache över senare komponentinitialiseringar. Att behålla båda egenskaperna demonstrerar den efterfrågade typrelationen, men en komponent som bara behöver den resulterande vyn kunde utelämna den lagrade providern.

Om komponenten ska begära innehåll när som helst dess body utvärderas, behåll bara providern. Följande är en ersättningskomponent-deklaration att använda med protokollet och appen ovan:

@MainActor
struct TileCard<Provider: TileProvider>: View {
    private let provider: Provider

    init(provider: Provider) {
        self.provider = provider
    }

    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text(provider.title)
                .font(.headline)
            provider.makeContent()
        }
        .padding()
        .frame(maxWidth: .infinity, alignment: .leading)
        .background(Color.gray.opacity(0.12))
    }
}

Båda designerna bevarar den associerade typen. Deras skillnad följer direkt av var metodanropet placeras: initialisering i den första designen, body-utvärdering i den andra. Välj den tidenpunkt medvetet, och håll innehållsproduktion fri från arbete som beror på att anropas exakt en gång.

Använd detta beslutsstöd när du anpassar exemplet:

Komponentkrav Deklarationsstrategi Tyrelation
Lagra providern och dess producerade vy Generisk provider plus Provider.Content Bevarar både provider-identitet och dess innehållstyp
Producera innehåll vid body-utvärdering Lagra den generiska providern Bevarar den associerade typen utan att lagra ett separat innehållsvärde
Lagra providers med en känd gemensam innehållstyp Överväg en begränsad existenziell Bevarar den specificerade innehållslikheten medan provider-identitet döljs
Medvetet radera olika vy-implementationer Använd AnyView vid en vald gräns Lagrar wrappertypen istället för att exponera den ursprungliga vitypen

Tar primära associerade typer bort behovet av generik?

Primära associerade typer erbjuder ett kortare sätt att uttrycka valda same-type-begränsningar. Om du äger protokollet gör den här alternativa headern Content primär; de övriga protokollkraven förblir desamma:

// Alternative protocol header, requiring Swift 5.7 or later.
@MainActor
protocol TileProvider<Content> {
    associatedtype Content: View

    var title: String { get }
    func makeContent() -> Content
}

Med den deklarationen beskriver any TileProvider<Text> en existenziell vars associerade innehållstyp är känd vara Text. Olika provider-implementationer kan dela den lagringstypen bara om deras konformenser exponerar den exakta innehållslikheten. Syntaxen begränsar en associerad typ; den gör inte protokollet till en generisk struct. Se SE-0346 och SE-0353.

Detta kombinerar inte godtyckliga okända vityper till en bevarad konkret typ. Särskilt exponerar CircleTile ett opakt innehållsresultat, så anroparna kan inte enkelt behandla det resultatet som Text. Använd den ursprungliga generiska komponenten när providerns associerade innehåll ska förbli obegränsat.

Begränsade existentiella implementerades i Swift 5.7, och SE-0353 identifierar explicit körtids- och bakåtkompatibilitetsgränser. Endast kompilatorstöd är därför otillräckligt för att upprätta distributionskompatibilitet för den valfria designen. Kontrollera målinstrumentkedjans tillgänglighetsdiagnostik innan du antager det. Huvudexemplet beror inte på begränsad existentiell lagring. Se SE-0353:s ABI-diskussion.

Vad bör du kontrollera när kompileringen misslyckas?

Spåra typen från protokollkravet till den lagrade egenskapen innan du ändrar layouten:

  1. Bekräfta att protokollet deklarerar en associerad typ begränsad till View.
  2. Bekräfta att komponenten har en namngiven generisk provider-parameter begränsad till det protokollet.
  3. Deklarera lagrat innehåll som Provider.Content, matchande metodens returtyp.
  4. Kontrollera om en förälder redan konverterat providern till any TileProvider.
  5. Om du lägger till en modifierare innan lagring, kontrollera dess resultatyp: den modifierade vyn kanske inte längre är Provider.Content.
  6. Kontrollera aktör-isoleringsdiagnostik separat från associerad-typ-diagnostik.

Apple dokumenterar att vymodifierare wrap-par en vy i en annan vy. Konsekvensen är att tilldelning av provider.makeContent().padding() till en egenskap deklarerad som Provider.Content ändrar det kontrakt tilldelningen måste uppfylla. Lagra det ursprungliga innehållet och applicera presentationsmodifierare i body, eller designa om den lagrade typen för att beskriva det modifierade resultatet. Se View.

Byggare har också en versionsgräns. Apples nuvarande dokumentation beskriver ViewBuilder-beteende för Xcode 26 och tidigare, sedan annorlunda innehållsbyggarbeteende från och med Xcode 27. Exemplet undviker ett anpassat builder-krav: varje provider returnerar ett explicit uttryck. Om du utökar det med builder-driven provider-API:er, följ dokumentationen för den Xcode-version du faktiskt bygger med. Se ViewBuilder.

Om du medvetet väljer AnyView, ta hänsyn till dess dokumenterade beteende: att ändra den wrappta vitypen förstör den gamla hierarkin och skapar en ny. Det är en separat designbeslut från att bevara en associerad typ i lagring. Apple dokumenterar detta i AnyView.

För den här komponenten, håll relationen synlig vid dess lagringsgräns: en generisk Provider, ett resultat av Provider.Content, och en lagrad egenskap av samma typ. Du kan sedan ändra en providers interna layout medan kompilatorn bär dess associerade vitype genom komponenten.

Källor