Als je verschillende getypeerde voltooiingen op één macOS SwiftUI-scherm wilt ontvangen, geef je elk component een generieke adapter of een getypeerde closure. Swift staat niet toe dat het scherm twee keer aan hetzelfde protocol voldoet met verschillende geassocieerde typen.

Deze tutorial bouwt een kleine desktoppreview met knoppen die een synthetische titel en een kolomaantal instellen. De uitdaging is om het voltooiingscontract van elk component door de compiler te laten controleren en tegelijk één bovenliggende view beide waarden te laten ontvangen.

Waarom kan één type niet aan beide voltooiingscontracten voldoen?

Het geassocieerde type van een protocol hoort bij de conformance. Als een voltooiingsprotocol een geassocieerd type met de naam Value declareert, moet een conformerend type daarvoor één consistente keuze maken. Twee overbelaste voltooiingsmethoden toevoegen, levert geen twee onafhankelijke conformances op. De taalgids van Swift legt uit hoe geassocieerde typen hun concrete typen krijgen wanneer een protocol wordt overgenomen. Generics — Associated Types.

Een scherm kan bijvoorbeeld niet één keer aan een voltooiingsprotocol voldoen met Value gelijk aan TitleChoice en nogmaals met Value gelijk aan LayoutChoice. De declaraties naar afzonderlijke extensies verplaatsen verandert die beperking niet. SE-0143 handhaaft expliciet het verbod op herhaalde conformance, ook voor conditionele conformances met elkaar uitsluitende voorwaarden. Conditional conformances.

Primaire geassocieerde typen nemen deze beperking niet weg. De syntaxis CompletionHandling<TitleChoice> drukt een beperking uit op een geassocieerd type; die creëert geen afzonderlijke protocolidentiteit voor elk argument. SE-0346 introduceerde deze beperkingssyntaxis in Swift 5.7. Lightweight same-type requirements for primary associated types.

Het ondersteunde ontwerp hieronder verplaatst de conformance naar een generieke adapter. Er is één conformance-declaratie en de applicatie maakt voor de twee componenten adapterwaarden met verschillende specialisaties. De bovenliggende view beheert de resulterende status zonder het voltooiingsprotocol over te nemen.

Welk voltooiingsontwerp past bij je component?

Gebruik deze besliswijzer voordat je de interface aanpast. Dit zijn ontwerpaanbevelingen op basis van de gedocumenteerde typeregels, geen aanvullende vereisten van de Swift-taal.

Vereiste van het component Aanbevolen ontwerp Wat expliciet blijft
Eén callback en je beheert de interface Een getypeerde closure Het invoertype van de callback
Een bestaande API op basis van een protocol Een generieke conformerende adapter Het protocol en het geassocieerde type
Eén gebeurteniskanaal met meerdere benoemde alternatieven Een enum met geassocieerde waarden Elke gebeurteniscase die de ontvanger afhandelt
Verschillende losstaande bewerkingen met verschillende betekenissen Afzonderlijke benoemde protocollen De afzonderlijke vereisten van elke bewerking

Neem voor dit voorbeeld aan dat het herbruikbare component al een handler op basis van een protocol verwacht. Een generieke adapter behoudt die grens en houdt de afhandelingscode van het scherm bij de statusupdates. Als het component alleen een callback nodig heeft en geen protocol vereist, is een closure het eenvoudigere ontwerp.

Een generieke voltooiingsmethode zou een andere belofte uitdrukken. Een methode zoals complete<T>(_ value: T) moet werken voor elk type dat aan de beperkingen voldoet. Daarmee druk je niet de beperktere vereiste uit dat elk component één bepaald resultaattype oplevert. De documentatie over generics van Swift beschrijft hoe generieke functies werken met de typen die op hun aanroeppunten worden gekozen. Generics — Generic Functions.

Bouw de getypeerde voltooiingsadapter

Gebruik voor dit voorbeeld een macOS SwiftUI-appdoel met een compiler die Swift 5.7 of later ondersteunt en een implementatiedoel voor macOS 12 of later. De compilervereiste komt voort uit de declaratie van het primaire geassocieerde type. Het implementatiedoel is een voorzichtige basis voor deze tutorial en betekent niet dat elke getoonde API voor het eerst in macOS 12 verscheen.

De drie onderstaande bestanden vormen samen het complete voorbeeld. Voeg ze toe aan hetzelfde appdoel en vervang het gegenereerde ingangspunt van de app en de content view, zodat het doel slechts één @main-declaratie heeft. Er zijn geen packages, assets, voorbeeldbestanden of externe invoer nodig.

Maak CompletionContracts.swift met deze declaraties:

import SwiftUI

struct TitleChoice {
    let text: String
}

struct LayoutChoice {
    let columns: Int
}

@MainActor
protocol CompletionHandling<Value> {
    associatedtype Value
    func complete(_ value: Value)
}

@MainActor
struct CompletionAdapter<Output>: CompletionHandling {
    typealias Value = Output

    private let receive: @MainActor (Output) -> Void

    init(_ receive: @escaping @MainActor (Output) -> Void) {
        self.receive = receive
    }

    func complete(_ value: Output) {
        receive(value)
    }
}

@MainActor
struct CompletionButton<Value, Handler: CompletionHandling>: View
where Handler.Value == Value {
    let title: String
    let value: Value
    let handler: Handler

    var body: some View {
        Button(action: {
            handler.complete(value)
        }) {
            Text(title)
        }
    }
}

De beperking Handler.Value == Value koppelt de uitvoer van het component aan de invoer die de handler accepteert. Een knop met een TitleChoice vereist dus een handler waarvan het geassocieerde type TitleChoice is. De expliciete typealias maakt het bijbehorende type van de adapter eenvoudig te controleren.

De adapter bewaart de closure om die later aan te roepen; daarom markeert de initializer die parameter met @escaping. De documentatie van Swift over closures vereist deze annotatie wanneer een closure langer kan bestaan dan de functieaanroep die de closure ontvangt. Closures — Escaping Closures.

Het protocol, de adapter en de view gebruiken @MainActor, en de opgeslagen callback behoudt die isolatie in het functietype. Zo maakt het bedoelde uitvoeringscontext van de UI deel uit van het contract. SE-0316 documenteert zowel de isolatie van globale actors als functietypen met kwalificatie door een globale actor. Global actors.

Apple documenteert Button als een knop met een actie en een label. Hier roept de actie de handler aan met de al getypeerde waarde van het component; Text(title) levert het zichtbare label. Button.

Koppel twee resultaatt typen aan één SwiftUI-scherm

Maak ContentView.swift. De titel- en lay-outresultaten hebben afzonderlijke bestemmingen voor hun status, en elke adapter-closure werkt alleen de bijbehorende bestemming bij.

import SwiftUI

@MainActor
struct ContentView: View {
    @State private var previewTitle = "Sample Board"
    @State private var columnCount = 1

    var body: some View {
        VStack(alignment: .leading, spacing: 16) {
            Text("Component Preview")
                .font(.headline)

            Text(previewTitle)
            Text("Columns: \(columnCount)")

            HStack {
                CompletionButton(
                    title: "Apply title",
                    value: TitleChoice(text: "Pebble Board"),
                    handler: CompletionAdapter<TitleChoice> { choice in
                        previewTitle = choice.text
                    }
                )

                CompletionButton(
                    title: "Apply layout",
                    value: LayoutChoice(columns: 3),
                    handler: CompletionAdapter<LayoutChoice> { choice in
                        columnCount = choice.columns
                    }
                )
            }
        }
        .padding(24)
        .frame(minWidth: 420, minHeight: 180)
    }
}

Maak TypedCompletionsApp.swift als het enige ingangspunt van de applicatie:

import SwiftUI

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

De bovenliggende view bewaart de beginwaarden in private @State-properties. Apple documenteert dat SwiftUI deze opslag beheert en afhankelijke views bijwerkt wanneer de waarden veranderen. De huidige documentatie vermeldt ook dat builds met Xcode 27 of later in plaats daarvan de State-macro gebruiken; de declaratiestijl hier volgt de eerdere basis met property wrappers. State.

WindowGroup bevat de viewhiërarchie van het scherm. Apple documenteert afzonderlijke statusopslag voor elk venster dat door de groep wordt aangemaakt, dus in dit voorbeeld hoort de status bij elk previewvenster. WindowGroup.

Er zijn twee gespecialiseerde adaptertypen: CompletionAdapter<TitleChoice> en CompletionAdapter<LayoutChoice>. Beide komen voort uit dezelfde generieke adapterdeclaratie. ContentView voldoet alleen aan View; de twee resultaten komen binnen via de closures die de view aanlevert.

Controleer het gedrag en spoor typeverschillen op

In de onderzoeksomgeving is geen Swift-compiler geïnstalleerd, dus de bestanden zijn gecontroleerd maar niet gecompileerd of uitgevoerd. Bouw het appdoel op je Mac en gebruik daarna de volgende controles om het bedoelde gedrag van het voorbeeld te verifiëren.

  1. Start de app. De preview moet Sample Board en Columns: 1 tonen.
  2. Klik op Apply title. De titel moet veranderen in Pebble Board, terwijl het kolomaantal hetzelfde blijft.
  3. Klik op Apply layout. Het aantal moet veranderen in Columns: 3, terwijl de gekozen titel hetzelfde blijft.
  4. Klik nogmaals op een van beide knoppen. Dezelfde synthetische waarde moet opnieuw worden ingesteld.

Om de typegrens te controleren, wijzig je tijdelijk de handlerspecialisatie van de eerste knop in CompletionAdapter<LayoutChoice> en geef je de closure een lege body. Laat de waarde van de knop op TitleChoice staan. Het component vereist dan via Handler.Value == Value dat onverenigbare typen gelijk zijn, dus de compilatie moet mislukken. Zet de oorspronkelijke handler daarna terug.

Als de compiler een melding geeft over herhaalde conformance, zoek dan naar een andere declaratie waardoor hetzelfde nominale type aan CompletionHandling voldoet. Behoud de ene conformance van de generieke adapter en maak voor de extra uitvoer nog een adapterwaarde. Twee extensies met beperkingen die dezelfde conformance declareren, vallen nog steeds onder de beperking van Swift op herhaalde conformances. Conditional conformances — Multiple conformances.

Als de compiler de syntaxis voor het primaire geassocieerde type van het protocol afwijst, controleer dan de compilerversie. Voor een oudere compiler kan de adapter hetzelfde ontwerp behouden met een gewone declaratie van het geassocieerde type. Hieronder staat alleen een vervangend fragment voor de protocoldeclaratie; laat de overige declaraties ongewijzigd:

@MainActor
protocol CompletionHandling {
    associatedtype Value
    func complete(_ value: Value)
}

Die wijziging neemt de afhankelijkheid van de Swift 5.7-syntaxis in dit voorbeeld weg. De compilervereiste voor @MainActor, geïntroduceerd in Swift 5.5, blijft gelden en het implementatiedoel van de tutorial verandert niet. Het component gebruikt al een generieke where-clausule, dus op de gebruikslocatie is geen syntaxis voor primaire geassocieerde typen nodig. Lightweight same-type requirements, Global actors.

Wat garandeert dit voltooiingscontract?

Het voorbeeld garandeert dat het payloadtype bij elke componentgrens overeenkomt. De implementatie roept de handler eenmaal aan per activering van een knop, synchroon. Er is geen asynchrone bewerking, annulering, persistentie of een voltooiingslevenscyclus die slechts eenmaal kan plaatsvinden geïmplementeerd. Daarvoor zijn aanvullende functionaliteit en een expliciet contract nodig.

Het voorbeeld gebruikt ook bekende lokale waarden. Als een toekomstig component willekeurige kolomaantallen accepteert, valideer die invoer dan voordat je de preview bijwerkt en bepaal hoe afwijzing wordt gemeld. Alleen een overeenkomend Swift-type garandeert niet dat een geheel getal een bruikbaar kolomaantal is.

Houd voor deze desktoptaak één adapter per getypeerd eindpunt aan en laat de bovenliggende view de resulterende status beheren. Zo krijgen de twee componenten precieze voltooiingscontracten via één herbruikbare conformancedeclaratie, waarbij de bestemming van elk resultaat zichtbaar is op de aanroeplocatie.

Bronnen