Om en enda delegat ska kunna hantera flera SwiftUI-komponenttyper gör du återanropsmetoden generisk och håller komponenten oberoende av delegatens konkreta typ. Den här guiden bygger en liten macOS-rutväljare som bevarar avsändarens innehållstyp utan ett rekursivt Self-krav.
Välj återanropskontraktet innan du väljer syntax
Uppgiften är tydlig: bygg en återanvändbar SwiftUI-komponent vars innehåll kan variera, samtidigt som en delegat tar emot urvalsåteranrop från alla innehållsspecialiseringar som stöds. Demonstrationen använder två syntetiska rutor – text och en cirkel – och visar identifieraren för den valda rutan.
Swift definierar delegering som ett protokollbaserat sätt för en klass eller struktur att överlåta ansvar till en annan instans. En delegat kan ta emot en åtgärd tillsammans med dess avsändare, som i delegeringsexemplet i språkhandboken. Protokoll
För den här komponenten är det avsedda kontraktet: ”För varje innehållstyp som följer View kan delegaten ta emot en ruta som innehåller det innehållet.” Placera typparametern på återanropsmetoden. Swifts generiska funktioner väljer sina konkreta typargument vid varje anrop, medan en associerad typ fastställs av en protokollkonformans. Det är två olika åtaganden. Generics
| Krav | Lämplig gränssnittsform | Konsekvens |
|---|---|---|
| En delegat hanterar alla innehållstyper som stöds | En generisk återanropsmetod | Varje återanrop kan ha en annan innehållstyp. |
| Varje delegat hanterar en fast innehållstyp | En associerad typ | Varje konformans anger en innehållstyp. |
| Återanropet behöver bara en rutidentifierare | Ett återanrop som tar identifieraren | Återanropet är inte beroende av återgivningstypen. |
Tabellen är ett designstöd som bygger på dessa språkregler. Välj den första raden när det är viktigt att API:t bevarar den typade avsändaren. Välj den tredje när en avsändaridentifierare ger mottagaren all information den behöver; då skulle det skapa ett onödigt beroende att skicka med en hel komponent.
Ta bort beroendet av delegatens egen typ
En hårt kopplad design kan göra komponenten generisk över sin delegat och sedan referera till den komponenten som Component<Self> i delegatprotokollet. Här betyder Self den slutliga typen som följer protokollet. Associerade typer tillhör på motsvarande sätt den typen. Deklarationer
Den formen gör kontrollen av återanropet beroende av en komponentspecialisering som i sin tur beror på delegaten. För att avgöra om en viss rekursiv deklaration godtas behöver man granska dess fullständiga begränsningar och kompilatorns diagnostik. Det vore felaktigt att säga att Swift avvisar alla rekursiva protokollrelationer: SE-0157 lade till rekursiva begränsningar för associerade typer i Swift 4.1. Stöd för rekursiva begränsningar för associerade typer
För uppgiften här behöver delegatens konkreta typ inte alls ingå i komponenttypen. Designen nedan ger Tile en generisk parameter, Content. Den lagrade delegaten är any TileDelegate, och protokollkravet introducerar en egen Content-parameter på metodnivå. Återanropet bevarar avsändarens innehållstyp utan att uttrycka den via delegatens Self.
Skrivsättet any betecknar uttryckligen ett existentiellt värde: det lagrade värdet kan innehålla en instans av en typ som följer protokollet. SE-0335 introducerade det skrivsättet i Swift 5.6. Att använda ett existentiellt värde för delegaten kräver inte att rutans innehåll typkonverteras till AnyView. Introducera existentiella any
Bygg en komplett macOS-rutväljare
Använd ett macOS SwiftUI-appmål med en Swift 6-kompilator, till exempel kompilatorn som medföljer Xcode 16 eller senare. Sätt macOS-distributionsmålet till 11.0 eller senare för exempelns @StateObject-lagring. Apple anger att StateObject är tillgängligt från macOS 11.0. StateObject
Ersätt den genererade appdeklarationen och startvyn med den här enda källfilen. Kontrollera att målet bara innehåller en @main-appdeklaration; Swift tillåter bara ett sätt att ange programmets startpunkt. Deklarationer
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()
}
}
}
Det här är ett komplett appexempel med importer, delegat, komponent, modell och startpunkt. Det granskades men kompilerades eller kördes inte här eftersom den tillgängliga miljön saknar Swift-kompilator och macOS-SDK.
Apples Button-API tar emot en åtgärd och en etikett som skapar en vy. I det här exemplet anropar ett klick på en ruta delegaten från knappens åtgärd; utvärdering av body skapar gränssnittet utan att skicka en urvalshändelse. Knapp
Bygg och kör appen på en Mac. Till en början bör statusen vara No tile selected. Om du klickar på Choose text bör statusen bli Selected: text; om du klickar på Choose circle bör den bli Selected: circle. Det här är förväntade resultat från exemplet, inte rapporterade körresultat.
Förstå vad exemplet bevarar
Den första rutan har typen Tile<Text> och den andra har typen Tile<Circle>. Båda använder samma SelectionModel-instans. Dess generiska återanrop uppfyller protokollkravet för båda innehållstyperna och läser samtidigt den gemensamma egenskapen id. Det är den praktiska fördelen med att placera den generiska parametern på metoden.
Mottagaren kan också komma åt tile.content som Content. Inne i den generiska implementationen garanterar den deklarerade begränsningen dock bara de egenskaper som View tillhandahåller. Kod som kräver en textspecifik åtgärd behöver ett annat kontrakt eller ytterligare information; återanropet kan inte förutsätta att varje innehållsvärde är Text. Det här följer av Swifts begränsningsbaserade generiska modell. Generics
Komponenten implementerar body med some View, i enlighet med Apples dokumenterade mönster för anpassade vyer. Den aktuella View-deklarationen är också isolerad till huvudaktören. Exemplet markerar uttryckligen det användargränssnittsinriktade protokollet och typerna med @MainActor så att återanropet och modelluppdateringen delar samma isolering. Vy
@StateObject tillhandahåller stabil modellagring under hela den omslutande vyns identitet. Ändringar i modellens publicerade urval uppdaterar den beroende statusvyn. Om du flyttar modellskapandet till ett vanligt beräknat uttryck beskriver du inte längre det ägarskapet genom StateObject. StateObject
Exemplet lagrar en obligatorisk delegat med stark referens. Dess modell lagrar bara urvalssträngen, så den illustrerade objektgrafen har ingen referens tillbaka till en ruta. Om du senare låter modellen behålla avsändare bör du se över ägarskapet. Apples språkhandbok visar en klassbunden delegat med svag lagring när relationen kräver det. Protokoll
När en associerad typ är ett bättre kontrakt
Anta att en mottagare bara ska acceptera textrutor. Ett alternativt protokoll kan deklarera associatedtype Content: View och kräva func didChoose(_ tile: Tile<Content>). Det uttrycker en innehållstyp per konformans. Om Content görs till en primär associerad typ får du smidig syntax för begränsningar; kravet blir inte en metod som accepterar alla innehållstyper. SE-0346 introducerade primära associerade typer i Swift 5.7. Lättviktiga krav på typlikhet för primära associerade typer
Det här alternativet skulle utgöra en annan API-version. Du skulle också ändra komponentens delegategenskap och initialiserarparameter till ett matchande begränsat existentiellt värde, till exempel any FixedTileDelegate<Content>, och låta varje mottagare ange sin fasta associerade typ och motsvarande icke-generiska återanrop. En konformans som bara gäller text kan då inte användas för cirkelrutan genom samma konformans.
SE-0353 lade till begränsade existentiella typer i Swift 5.7, vilket gör det möjligt att binda ett existentiellt värdes primära associerade typer till angivna typer. Att binda en associerad typ beskriver fortfarande en fast relation. Det tillhandahåller inte en samling återanropsimplementationer för godtyckliga innehållstyper. Begränsade existentiella typer
Håll stöd för kompilatorn åtskilt från stöd för distributionsmålet. SE-0353 nämner uttryckligen ändringar i körtiden som inte kan distribueras bakåt till befintliga OS-versioner. Innan du använder alternativet med ett äldre distributionsmål bör du kontrollera tillgänglighetsdiagnostiken i det faktiska målet. Det fullständiga exemplet ovan använder ett vanligt existentiellt delegatvärde och kräver inte lagring av begränsade existentiella värden. Begränsade existentiella typer
Felsök en misslyckad anpassning i rätt ordning
- Kontrollera återanropets avsedda omfattning. Om en delegat måste hantera både text- och cirkelrutor måste dess konformans behålla den generiska metodsignaturen. En metod som bara tar emot
Tile<Text>uppfyller inte det kravet. - Kontrollera komponentens generiska parametrar. I den här designen beror
TilepåContent; dess typ innehåller inte delegatens konkreta typ. - Kontrollera aktörisolering. Håll användargränssnittets återanrop och den illustrerade observerbara modellen på huvudaktören. Anrop från en annan isoleringskontext måste respektera den gränsen.
- Kontrollera var händelsen skickas. Skicka urvalet från knappens åtgärd, inte medan du beräknar
bodyeller initierar en vy. - Kontrollera modellens ägarskap. Behåll
@StateObjecti den omslutande vyn och undvik att skapa ytterligare en modellinstans för återanropet.
Om en anpassning fortfarande misslyckas kan du reducera den till en ruta och en delegatimplementation och sedan återinföra den andra innehållstypen. Den ordningen skiljer ett problem med protokollkonformans från ett problem med vykomposition. De två olika innehållstyperna i exemplet ger ett användbart kontraktstest: båda ska nå samma generiska återanrop utan typkonverteringar eller typborttagning.
Det viktiga designvalet är var typvariationen ska ligga. För en delegat som hanterar varje komponentspecialisering placerar du variationen på återanropsmetoden och håller komponentens typ oberoende av delegatens konkreta identitet. Resultatet är ett API som bevarar en typad avsändare och samtidigt håller urvalsflödet i macOS tillräckligt litet för att granska och bygga vidare på.
Källor
- Generics — The Swift Programming Language
- Protokoll — The Swift Programming Language
- Deklarationer — The Swift Programming Language
- Stöd för rekursiva begränsningar för associerade typer — Swift Evolution
- Introducera existentiella any — Swift Evolution
- Lättviktiga krav på typlikhet för primära associerade typer — Swift Evolution
- Begränsade existentiella typer — Swift Evolution
- Vy — Apple Developer Documentation
- StateObject — Apple Developer Documentation
- Knapp — Apple Developer Documentation