Als één delegate meerdere SwiftUI-componenttypen moet afhandelen, maak je de callbackmethode generiek en houd je de component onafhankelijk van het concrete type van de delegate. Deze gids bouwt een kleine macOS-tegelkiezer die het inhoudstype van de afzender behoudt zonder een recursieve Self-vereiste.

Kies het callbackcontract voordat je de syntaxis kiest

De taak voor de lezer is specifiek: bouw een herbruikbare SwiftUI-component waarvan de inhoud kan variëren, terwijl één delegate selectiecallbacks ontvangt van elke ondersteunde contentspecialisatie. De demonstratie gebruikt twee synthetische tegels—tekst en een cirkel—en toont de identifier van de geselecteerde tegel.

Swift definieert delegatie als een protocolgebaseerde manier waarop een klasse of structuur verantwoordelijkheden overdraagt aan een andere instantie. Een delegate kan een actie samen met de afzender ontvangen, zoals te zien is in het delegatievoorbeeld in de taalgids. Protocollen

Voor deze component is het beoogde contract: “Voor elk contenttype dat aan View voldoet, kan de delegate een tegel met die inhoud ontvangen.” Plaats de typeparameter op de callbackmethode. Generieke functies in Swift kiezen bij elke aanroep hun concrete typeargumenten, terwijl een associated type wordt vastgelegd door een protocolconformiteit. Dat zijn verschillende toezeggingen. Generics

Vereiste Geschikte interfacevorm Gevolg
Eén delegate handelt elk ondersteund contenttype af Een generieke callbackmethode Elke callback kan een ander contenttype hebben.
Elke delegate handelt één vast contenttype af Een associated type Elke conformiteit levert één contenttype.
De callback heeft alleen een tegelidentifier nodig Een callback met de identifier als argument De callback is niet afhankelijk van het rendertype.

Deze tabel is een ontwerphulpmiddel op basis van die taalregels. Kies de eerste rij wanneer het behoud van het getypeerde afzenderobject deel uitmaakt van de API. Kies de derde wanneer een afzenderidentifier alles biedt wat de ontvanger nodig heeft; de volledige component doorgeven zou dan een onnodige afhankelijkheid toevoegen.

Verwijder de afhankelijkheid van het eigen type van de delegate

Een sterk gekoppeld ontwerp kan de component generiek maken op basis van zijn delegate en vervolgens binnen het delegateprotocol naar die component verwijzen als Component<Self>. Hier betekent Self het uiteindelijke conformerende delegatetype. Associated types horen eveneens bij dat conformerende type. Declaraties

Die vorm maakt het controleren van de callback afhankelijk van een componentspecialisatie die zelf weer afhankelijk is van de delegate. Of een bepaalde recursieve declaratie wordt geaccepteerd, moet worden vastgesteld aan de hand van de volledige beperkingen en compilerdiagnostiek. Het zou onjuist zijn te stellen dat Swift elke recursieve protocolrelatie afwijst: SE-0157 voegde in Swift 4.1 recursieve beperkingen op associated types toe. Ondersteuning voor recursieve beperkingen op associated types

Voor de gekozen taak hoeft het concrete type van de delegate helemaal niet in het componenttype voor te komen. In het onderstaande ontwerp heeft Tile één generieke parameter: Content. De opgeslagen delegate is any TileDelegate, en de protocolvereiste introduceert een eigen Content-parameter op methodeniveau. De callback behoudt het contenttype van de afzender zonder dit uit te drukken via de Self van de delegate.

De schrijfwijze any duidt expliciet een existential-waarde aan: de opgeslagen waarde kan een instantie bevatten van een type dat aan het protocol voldoet. SE-0335 introduceerde die schrijfwijze in Swift 5.6. Het gebruik van een existential voor de delegate vereist niet dat de inhoud van de tegel wordt gewist naar AnyView. Introduceer existential any

Bouw een complete macOS-tegelkiezer

Gebruik een macOS SwiftUI-appdoel met een Swift 6-compiler, zoals de compiler die bij Xcode 16 of hoger wordt geleverd. Stel voor de @StateObject-opslag in dit voorbeeld het macOS-implementatiedoel in op 11.0 of hoger. Apple documenteert dat StateObject beschikbaar is vanaf macOS 11.0. StateObject

Vervang de gegenereerde appdeclaratie en startweergave door dit ene bronbestand. Zorg dat het doel slechts één @main-appdeclaratie bevat; Swift staat maar één aanpak voor het programma-ingangspunt toe. Declaraties

import SwiftUI
import Combine

@MainActor
protocol TileDelegate: AnyObject {
    func didChoose<Content: View>(_ tile: Tile<Content>)
}

@MainActor
struct Tile<Content: View>: View {
    let id: String
    let delegate: any TileDelegate
    let content: Content

    init(
        id: String,
        delegate: any TileDelegate,
        @ViewBuilder content: () -> Content
    ) {
        self.id = id
        self.delegate = delegate
        self.content = content()
    }

    var body: some View {
        Button(action: {
            delegate.didChoose(self)
        }) {
            VStack(spacing: 8) {
                content
                    .frame(width: 100, height: 50)
                Text("Choose \(id)")
            }
            .padding(8)
        }
    }
}

@MainActor
final class SelectionModel: ObservableObject, TileDelegate {
    @Published private(set) var selection = "No tile selected"

    func didChoose<Content: View>(_ tile: Tile<Content>) {
        selection = "Selected: \(tile.id)"
    }
}

@MainActor
struct ChooserView: View {
    @StateObject private var model = SelectionModel()

    var body: some View {
        VStack(spacing: 16) {
            HStack(spacing: 16) {
                Tile(id: "text", delegate: model) {
                    Text("Sample")
                }

                Tile(id: "circle", delegate: model) {
                    Circle()
                }
            }

            Text(model.selection)
        }
        .padding(24)
        .frame(minWidth: 340, minHeight: 180)
    }
}

@main
@MainActor
struct DelegateDemoApp: App {
    var body: some Scene {
        WindowGroup {
            ChooserView()
        }
    }
}

Dit is een compleet appvoorbeeld, inclusief imports, delegate, component, model en ingangspunt. Het is hier bekeken, maar niet gecompileerd of uitgevoerd, omdat de beschikbare omgeving geen Swift-compiler of macOS SDK heeft.

Apple’s Button-API accepteert een actie en een label dat een view oplevert. In dit voorbeeld roept het klikken op een tegel de delegate aan vanuit de knopactie; het evalueren van body bouwt de interface op zonder een selectiegebeurtenis te verzenden. Button

Bouw en voer de app uit op een Mac. Aanvankelijk hoort de status No tile selected te zijn. Als je op Choose text klikt, hoort Selected: text te verschijnen; als je op Choose circle klikt, hoort Selected: circle te verschijnen. Dit zijn verwachte resultaten van het voorbeeld, geen gerapporteerde uitvoeringsresultaten.

Begrijp wat het voorbeeld behoudt

De eerste tegel heeft het type Tile<Text> en de tweede het type Tile<Circle>. Beide gebruiken dezelfde instantie van SelectionModel. De generieke callback daarvan voldoet voor beide contenttypen aan de protocolvereiste en leest daarbij de gemeenschappelijke eigenschap id. Dat is het praktische voordeel van de generieke parameter op de methode plaatsen.

De ontvanger kan ook tile.content benaderen als Content. Binnen de generieke implementatie garandeert de gedeclareerde beperking echter alleen de mogelijkheden van View. Code die een tekstspecifieke bewerking vereist, heeft een ander contract of aanvullende informatie nodig; de callback kan niet aannemen dat elke contentwaarde Text is. Dit volgt uit het op beperkingen gebaseerde generieke model van Swift. Generics

De component implementeert body met some View, volgens Apple’s gedocumenteerde patroon voor aangepaste views. De huidige View-declaratie heeft ook main-actor-isolatie. In het voorbeeld worden de protocol- en UI-typen expliciet gemarkeerd als @MainActor, zodat de callback en de modelupdate dezelfde isolatie delen. View

@StateObject levert stabiele modelopslag gedurende de levensduur van de identiteit van de omvattende view. Wijzigingen aan de gepubliceerde selectie van het model werken de afhankelijke statusview bij. Als je het maken van het model verplaatst naar een gewone berekende expressie, druk je dat eigenaarschap niet langer uit met StateObject. StateObject

Het voorbeeld slaat een verplichte delegate sterk op. Het model bewaart alleen de selectietekst, dus de getoonde objectgraaf bevat geen verwijzing terug naar een tegel. Als je het model later afzenders laat bewaren, bekijk dan het eigenaarschap opnieuw. Apple’s taalgids toont een class-bound delegate met zwakke opslag wanneer die relatie dat vereist. Protocollen

Wanneer een associated type het betere contract is

Stel dat een ontvanger alleen teksttegels moet accepteren. Een alternatief protocol kan associatedtype Content: View declareren en func didChoose(_ tile: Tile<Content>) vereisen. Daarmee wordt één contenttype per conformiteit uitgedrukt. Van Content een primair associated type maken, levert handige syntaxis voor beperkingen op; het verandert de vereiste niet in een methode die elk contenttype accepteert. SE-0346 introduceerde primaire associated types in Swift 5.7. Lichtgewicht same-type-vereisten voor primaire associated types

Dit alternatief zou een andere API-versie zijn. Je zou ook de delegate-eigenschap en initialisatieparameter van de component wijzigen in de overeenkomstige beperkte existential, zoals any FixedTileDelegate<Content>, en elke ontvanger aanpassen zodat die zijn vaste associated type en bijbehorende niet-generieke callback levert. Een conformiteit die alleen tekst ondersteunt, kan via diezelfde conformiteit niet langer ook voor de cirkeltegel dienen.

SE-0353 voegde in Swift 5.7 beperkte existentialtypen toe, waarmee de primaire associated types van een existential aan opgegeven typen kunnen worden gekoppeld. Eén associated type vastleggen beschrijft nog steeds een vaste relatie. Het levert geen reeks callbackimplementaties voor willekeurige contenttypen. Beperkte existentialtypen

Houd compilerondersteuning en ondersteuning voor implementatiedoelen gescheiden. SE-0353 vermeldt expliciet runtimewijzigingen die niet achterwaarts kunnen worden uitgerold naar bestaande OS-versies. Controleer voordat je het alternatief op een ouder implementatiedoel toepast de beschikbaarheidsdiagnostiek voor het daadwerkelijke doel. Het volledige voorbeeld hierboven gebruikt een gewone delegate-existential en vereist geen opslag van beperkte existentials. Beperkte existentialtypen

Diagnoseer een mislukte aanpassing in een nuttige volgorde

  1. Controleer de beoogde reikwijdte van de callback. Als één delegate tekst- en cirkeltegels moet afhandelen, moet de implementatie de generieke methodesignatuur behouden. Een methode die alleen Tile<Text> accepteert, voldoet niet aan die vereiste.
  2. Controleer de generieke parameters van de component. In dit ontwerp hangt Tile af van Content; het type ervan bevat niet het concrete delegatetype.
  3. Controleer de actorisolatie. Houd UI-callbacks en het getoonde observeerbare model op de main actor. Aanroepen vanuit een andere isolatiecontext moeten die grens respecteren.
  4. Controleer waar de gebeurtenis wordt afgehandeld. Verzend de selectie vanuit de knopactie, niet tijdens het berekenen van body of het initialiseren van een view.
  5. Controleer het eigenaarschap van het model. Behoud @StateObject in de omvattende view en introduceer geen tweede modelinstantie voor de callback.

Als een aanpassing nog steeds niet werkt, beperk die dan tot één tegel en één delegate-implementatie en voeg daarna het tweede contenttype weer toe. Zo kun je een probleem met een protocolimplementatie onderscheiden van een probleem met de samenstelling van views. De twee verschillende contenttypen in het voorbeeld vormen een nuttige controle van het contract: beide moeten dezelfde generieke callback kunnen bereiken zonder casts of inhoudswissing.

De essentiële ontwerpbeslissing is waar de typevariatie thuishoort. Voor een delegate die elke componentspecialisatie afhandelt, plaats je die variatie op de callbackmethode en houd je het componenttype onafhankelijk van de concrete identiteit van de delegate. De resulterende API behoudt een getypeerde afzender en houdt de macOS-selectiestroom klein genoeg om te inspecteren en uit te breiden.

Bronnen