För ett macOS-fönster som visar heterogena SwiftUI-komponenter bör varje providers utdata behålla sin starka typning. Eliminera typen när det gemensamma renderingsgränssnittet behöver en enda konkret vytyp. Eliminera providertypen tidigare när själva samlingen behöver en enhetlig wrapper.

Den här artikeln bygger ett litet komponentgalleri med syntetisk text och en blå färgruta. Uppgiften är att lagra olika providerimplementeringar tillsammans och rendera dem genom ett gemensamt SwiftUI-gränssnitt. Designbeslutet gäller var man ska sluta exponera relationen mellan en provider och dess utdata.

Vad behöver providerkontraktet bevara?

En provider med en associerad utdatatyp anger att varje typ som följer protokollet väljer sin egen vytyp. En konkret implementation kan dölja det valet bakom some View, medan generiska konsumenter behåller relationen mellan providern och dess utdata. Swifts opaka typer bevarar typidentitet; boxade protokolltyper tillåter olika underliggande typer bakom ett gemensamt gränssnitt. Se Opaque and Boxed Protocol Types.

Följande är ett kontraktsfragment, inte en komplett applikation:

import SwiftUI

@MainActor
protocol ViewProvider {
    associatedtype Content: View
    var id: String { get }
    func makeView() -> Content
}

@MainActor
struct TitleProvider: ViewProvider {
    let id: String
    let title: String

    func makeView() -> some View {
        Text(title).font(.headline)
    }
}

Här härleds TitleProvider.Content från den opaka returtypen hos makeView(). Protokollet kräver en associerad typ, och implementationen tillhandahåller den genom härledning. SE-0244 dokumenterar uttryckligen det här mönstret som stöds samt begränsningen mot att deklarera en opak returtyp direkt som ett protokollkrav. Därför är det inte det kontrakt som stöds och beskrivs i förslaget att ersätta kravet med func makeView() -> some View. Se Opaque Result Types.

En array av any ViewProvider löser heterogen lagring. SE-0309 i Swift 5.7 tillåter existentiell användning av protokoll med associerade typer, samtidigt som begränsningar för enskilda medlemsoperationer kvarstår. När en utdata läses genom en sådan existential kan dess associerade typ reduceras till protokollets övre gräns—i det här fallet any View. Se Unlock existentials for all protocols.

Då får du två separata beslut: hur blandade providrar ska lagras och vilken konkret typ renderaren ska returnera. Apple dokumenterar AnyView som en konkret, typeliminerad vy som följer View, vilket gör den till ett uttryckligt alternativ för det andra beslutet. Se AnyView.

Välj gränsen utifrån konsumentens krav

Använd den här beslutstabellen innan du ändrar protokollet. Rekommendationerna tillämpar de dokumenterade typkontrakten på galleriuppgiften; de är designvägledning, inte uppmätta prestandaresultat.

Konsumentens krav Lämplig design Var typinformationen döljs
Rendera en konkret provider genom återanvändbar kod En generisk konsument med villkoret ViewProvider Behåll tillgänglig relationen mellan provider och utdata
Lagra olika providrar och behålla deras gemensamma metadata [any ViewProvider], med en renderingshjälpfunktion som returnerar AnyView Providertypen vid lagring; utdatatypen vid rendering
Exponera en konkret provider-wrapper för nedströmskod [AnyViewProvider] När wrappern skapas
Kräva att varje implementation returnerar en redan typeliminerad vy Ett protokollkrav som returnerar AnyView Inuti varje implementation
Visa en fast, liten uppsättning komponentalternativ En enum och en builderbaserad switch Alternativen förblir uttryckliga i enumen

För det här galleriet väljer du existentiell lagring och eliminering av utdatatypen. Providrarna behåller sina egna utdatatyper, samlingen exponerar en stabil strängidentifierare och en hjälpfunktion ansvarar för konverteringen till AnyView. Om en komponents interna layout ändras behöver den komponenten då ingen ny kod för typeliminering.

En generisk samlingsparameter skulle passa en annan uppgift. En array vars elementtyp är en enda generisk providertyp kan inte automatiskt representera flera orelaterade providertyper. Swifts implicita öppning av existentiella värden gäller ett enskilt existentiellt argument; den ger inte en heterogen array en gemensam underliggande typ som följer protokollet. Se Implicitly Opened Existentials.

Bygg ett komplett macOS-komponentgalleri

Exemplet kräver en kompilator för Swift 5.7 eller senare och ett macOS-mål för SwiftUI-appar. Ange macOS 12 eller senare som målversion för exemplet. I ett macOS App-projekt i Xcode som använder SwiftUI ersätter du den genererade appdeklarationen med hela programmet nedan och behåller exakt en @main-deklaration.

De uttryckliga @MainActor-annoteringarna ser till att providerskapandet och renderingen sker på samma aktör. Apples aktuella deklaration av View är isolerad till huvudaktören. Dokumentationen förklarar också att anpassade vyer tillhandahåller sitt innehåll genom body. Se View.

import SwiftUI

@MainActor
protocol ViewProvider {
    associatedtype Content: View
    var id: String { get }
    func makeView() -> Content
}

@MainActor
struct TitleProvider: ViewProvider {
    let id: String
    let title: String

    func makeView() -> some View {
        Text(title)
            .font(.headline)
    }
}

@MainActor
struct SwatchProvider: ViewProvider {
    let id: String

    func makeView() -> some View {
        RoundedRectangle(cornerRadius: 8)
            .fill(Color.blue)
            .frame(width: 96, height: 48)
    }
}

@MainActor
func erasedView<P: ViewProvider>(for provider: P) -> AnyView {
    AnyView(provider.makeView())
}

@MainActor
struct GalleryView: View {
    let providers: [any ViewProvider]

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            ForEach(providers, id: \.id) { provider in
                erasedView(for: provider)
            }
        }
        .padding(24)
        .frame(minWidth: 280, minHeight: 180)
    }
}

@main
@MainActor
struct ProviderGalleryApp: App {
    private let providers: [any ViewProvider] = [
        TitleProvider(id: "title", title: "Component gallery"),
        SwatchProvider(id: "swatch")
    ]

    var body: some Scene {
        WindowGroup {
            GalleryView(providers: providers)
        }
    }
}

Det förväntade resultatet är ett fönster som visar Component gallery ovanför en blå, rundad rektangel. Alla indata är lokala syntetiska värden. Exemplet har granskats men kompilerades inte här eftersom den tillgängliga miljön saknar Swift-kompilator och Apple SDK.

Hjälpfunktionens resultat är alltid AnyView, oavsett providerns associerade utdatatyp. Inuti hjälpfunktionen är providern generisk och relationen till dess Content-typ bevaras. Apple dokumenterar initialiseraren som används här som en funktion som tar emot ett generiskt värde med villkoret View och eliminerar dess typ. Se AnyView.init(_:).

Samlingen använder id: \.id eftersom protokollet låser id till String; den kräver inte att någon annan okänd associerad typ återställs. Apples dokumentation för ForEach tillåter antingen element som kan identifieras eller en uttrycklig identifierande key path. Se till att identifierarna är unika i samlingen och stabila för de logiska komponenterna mellan uppdateringar. Se ForEach.

Varför fungerar en generisk hjälpfunktion när en generisk vy-wrapper misslyckas?

Den viktiga egenskapen hos erasedView är dess konkreta returtyp. Swift 5.7 introducerade implicit öppning av existentiella argument, vilket låter kompilatorn binda en generisk parameter till ett existentiellt värdes underliggande typ vid ett funktionsanrop. Begränsningar gäller fortfarande för resultat som skulle exponera den öppnade typen genom en invariant generisk behållare. SE-0352 dokumenterar också ett bredare beteende för öppning som aktiveras i Swift 6:s språkläge. Se Implicitly Opened Existentials.

Följande diagnostikfragment lägger till en generisk wrapper i programmet ovan. Det kommenterade anropet visar den problematiska gränsen; det är ingen ytterligare startpunkt för applikationen:

@MainActor
struct ProviderRow<P: ViewProvider>: View {
    let provider: P

    var body: some View {
        provider.makeView()
    }
}

@MainActor
func inspectBoundary(_ provider: any ViewProvider) {
    // Problematic: the constructed result would retain the opened P.
    // let row = ProviderRow(provider: provider)

    // Supported boundary: the result is the concrete type AnyView.
    let row = erasedView(for: provider)
    _ = row
}

Detta förklarar varför ett extra generiskt lager i sig kanske inte löser felet. Granska både parametern och resultatet. En hjälpfunktion som returnerar en konkret typeliminerad vy slutför konverteringen i anropet; ett resultat som ProviderRow<P> behåller providertypen i sin struktur.

På samma sätt är some View inte ett sätt att skriva ett returvärde vars underliggande typ godtyckligt kan variera vid körning. Implementationens underliggande typ är konsekvent, även om den kan bero på generiska argument. En builder kan koda grenar som ett sammansatt resultat, men en deklaration av en opak returtyp förenar inte i sig orelaterade returtyper. Se Opaque and Boxed Protocol Types.

När bör du eliminera själva providertypen?

Välj en konkret wrapper när nedströmsgränssnitt ska ta emot en enda providertyp, eller när du vill att anropare normaliserar providrar innan de lagras. Följande tilläggsdeklaration fungerar med det befintliga protokollet och providerna i exemplet. Den kapslar in providern och skapar dess vy när makeView() anropas.

@MainActor
struct AnyViewProvider: ViewProvider {
    let id: String
    private let build: @MainActor () -> AnyView

    init<P: ViewProvider>(_ provider: P) {
        id = provider.id
        build = { AnyView(provider.makeView()) }
    }

    func makeView() -> AnyView {
        build()
    }
}

Om du väljer det här alternativet ändrar du båda lagrade egenskaperna providers i det kompletta programmet till [AnyViewProvider]. Ersätt appens ursprungliga array med följande uttryck:

[
    AnyViewProvider(
        TitleProvider(id: "title", title: "Component gallery")
    ),
    AnyViewProvider(SwatchProvider(id: "swatch"))
]

Ersätt sedan erasedView(for: provider) i galleriets ForEach-closure med provider.makeView(). Wrappern returnerar redan AnyView, så anropet behöver ingen ytterligare konvertering. Implementation kopierar identifieraren och kapslar in det angivna providervärdet; det är inte ett sätt att senare ersätta det inkapslade värdet.

Om varje konsument behöver typeliminerad utdata kan ett enklare protokoll kräva makeView() -> AnyView direkt. Det minskar mängden adapterkod men gör varje typ som följer protokollet ansvarig för typelimineringen. Välj detta när typeliminerad utdata är ett avsiktligt löfte i gränssnittet. Välj kontraktet med associerad typ när generiska konsumenter fortfarande ska kunna använda relationen mellan varje provider och dess utdata.

Ta hänsyn till kompilatorns begränsningar och vyns identitet

Håll språkfunktioner åtskilda från builderbeteende. Exemplet bygger på existentiella funktioner i Swift 5.7 och ett uttryckligt AnyView-resultat. Apples aktuella dokumentation gör skillnad mellan Xcode 26 och tidigare versioner samt Xcode 27 och senare: de senare skapar builderinnehåll med typagnostisk hantering och rekommenderar ContentBuilder för det beteendet. Dokumentationen beskriver fortfarande typsäkerhet genom villkorliga konformanser. Tolka inte lättnader i builderns indatakontroller som ett allmänt löfte om att en existential i sig uppfyller alla generiska View-villkor. Se ViewBuilder och ContentBuilder.

För en fast uppsättning komponenter kan du överväga en enum vars fall innehåller komponentdata och vars renderingsproperty växlar mellan fallen. Då blir de alternativ som stöds tydliga. Den designen passar när en ny komponent ska kräva en uppdatering av en central renderare; providerlösningen passar gränssnitt där implementationerna själva ska tillhandahålla renderingen.

Typeliminering får också en dokumenterad konsekvens för identiteten. Apple anger att om typen som omsluts av en AnyView ändras förstörs den föregående hierarkin och en ny skapas. En stabil identifierare i samlingen åsidosätter inte det beteendet. Om en komponent kan växla mellan olika omslutna vytyper bör du kontrollera dess tillstånd och övergångsbeteende i den faktiska appen. Se AnyView.

För det här galleriet bör du behålla providerkontraktet med associerad typ, lagra de blandade värdena som existentialer och returnera AnyView från en enda renderingshjälpfunktion. Flytta typelimineringen till en konkret provider-wrapper när nedströmsgränssnitt behöver den enhetliga typen. Då finns en tydlig gräns att granska när samlingen, renderingskontraktet eller kompilatorkonfigurationen ändras.

Källor