Gebruik een concrete Hashable-wrapper met een initializer die aan een protocol is gebonden om verschillende SwiftUI-shapes in één set op te slaan en tegelijk een gedeelde interface af te dwingen. In deze tutorial bouwen we een klein Mac-palet met shapes, expliciete regels voor duplicaten en aanroepbare shape-bewerkingen.
De opdracht is specifiek: verzamel synthetische schijf- en tegelcomponenten, wijs niet-gerelateerde waarden al tijdens het compileren af, verwijder duplicaten en toon de overgebleven componenten in een SwiftUI-venster. De verzameling accepteert meerdere componenttypen via een gedeeld protocol dat SwiftUI’s Shape verfijnt.
Waarom voldoet een set met protocolwaarden niet aan deze voorwaarden?
Swift vereist dat het elementtype van Set voldoet aan Hashable. De declaratie dat elke waarde binnen een existential voldoet aan Hashable geeft dat existential type zelf geen conformance. Daardoor wordt Set<any PaletteShape> niet geldig alleen omdat PaletteShape Hashable erft. Apple documenteert de elementbeperking van de verzameling, terwijl Swift Evolution het onderscheid uitlegt tussen een existential-container en de onderliggende waarde die het protocol implementeert. Set, Impliciet geopende existentials.
Een existential zoals any PaletteShape kan verschillende typen bevatten die het protocol implementeren. Het probleem zit in de gelijkheid: de overgeërfde vereiste Equatable vergelijkt twee waarden van hetzelfde Self-type, terwijl twee existential-containers verschillende concrete typen kunnen bevatten. De uitgebreidere ondersteuning van Swift voor existentialwaarden biedt geen universele gelijkheidsbewerking voor die containers. Existentials ontgrendelen voor alle protocollen.
Deze alternatieven lossen verschillende delen van het probleem op:
| Representatie | Meerdere concrete typen? | Gedeelde interface afgedwongen? | Geschikt als element van een set? |
|---|---|---|---|
[any PaletteShape] |
Ja | Ja | Een array garandeert geen uniciteit zoals een set |
Set<S> met S: PaletteShape |
Eén gekozen S |
Ja | Ja, voor dat ene concrete type |
Set<AnyHashable> |
Ja | Geen shape-beperking | Ja |
Set<AnyPaletteShape> hieronder |
Ja, binnen wrappers | Ja, bij constructie | Ja |
AnyHashable biedt type-uitgewiste gelijkheid en hashing, maar de initializer accepteert in het algemeen hashable waarden. Een set die alleen dit type bevat, zou dus ook een string of integer kunnen accepteren. De aangepaste wrapper hieronder beperkt de invoer tot het shape-protocol van de applicatie en bewaart de shape-interface naast de type-uitgewiste hashable waarde. AnyHashable.
Wanneer geldt een shape als duplicaat?
Bepaal de gelijkheid voordat je de wrapper implementeert. In dit palet zijn twee componenten duplicaten wanneer ze hetzelfde concrete componenttype en gelijke opgeslagen waarden hebben. Een schijf met het label Sample en een tegel met het label Sample blijven afzonderlijke elementen. Twee schijven met hetzelfde label worden samengevoegd tot één element.
Het label is de enige opgeslagen eigenschap in elk voorbeeldcomponent, dus de automatisch gesynthetiseerde gelijkheid en hashing omvatten dit. Dit is een illustratieve keuze voor het model: visuele overeenkomst alleen bepaalt geen gelijkheid. Apple documenteert de automatische synthese van Hashable voor structuren waarvan de opgeslagen eigenschappen hashable zijn, en vereist dat gelijke waarden dezelfde onderdelen aan hun hashers doorgeven. Hashable.
De wrapper neemt ook een type-identificatie op in zowel de gelijkheid als de hashing. Zo wordt de regel voor hetzelfde type expliciet gemaakt, zonder afhankelijk te zijn van aannames over vergelijkingen na type-uitwissing. AnyHashable kan bepaalde waarden met verschillende onderliggende typen vergelijken aan de hand van canonieke representaties; Apple definieert het niet als een onvoorwaardelijke onderscheider van concrete typen. ObjectIdentifier ondersteunt metatypen, zodat dit voorbeeld het generieke invoertype kan identificeren. AnyHashable, ObjectIdentifier.
Bouw het volledige Mac-paletvoorbeeld
Gebruik in Xcode een SwiftUI-appproject voor macOS met een compiler van Swift 5.7 of nieuwer. Kies voor een reproduceerbare basis Swift 5-taalmodus en een macOS-implementatiedoel van 13.0 of later. Het implementatiedoel is een conservatieve keuze voor deze tutorial, geen bewering dat elke onderstaande API macOS 13 vereist.
De minimale compilerversie is van belang omdat het voorbeeld any PaletteShape opslaat, waarvan de overgeërfde protocollen vereisten met geassocieerde typen en Self bevatten. SE-0309 vermeldt dat deze uitgebreidere existential-ondersteuning is geïmplementeerd in Swift 5.7. Het voorbeeld vereist geen nieuwere functie voor algemene conformance van existentials. Existentials ontgrendelen voor alle protocollen.
Vervang de gegenereerde app-declaratie en content view door het volgende volledige bronbestand. Zorg dat er in het app-target maar één @main-declaratie staat. Het voorbeeld bevat de componentdefinities, wrapper, voorbeeldwaarden, controles, view en app-entrypoint. Swift is niet beschikbaar in de onderzoeksomgeving, dus de code is nagekeken maar hier niet gecompileerd of uitgevoerd.
import SwiftUI
protocol PaletteShape: Shape, Hashable {
var label: String { get }
}
struct Disk: PaletteShape {
let label: String
func path(in rect: CGRect) -> Path {
Path(ellipseIn: rect)
}
}
struct Tile: PaletteShape {
let label: String
func path(in rect: CGRect) -> Path {
var result = Path()
result.addRect(rect)
return result
}
}
struct AnyPaletteShape: Hashable {
private let base: any PaletteShape
private let typeID: ObjectIdentifier
private let key: AnyHashable
init<S: PaletteShape>(_ value: S) {
base = value
typeID = ObjectIdentifier(S.self)
key = AnyHashable(value)
}
var label: String { base.label }
func path(in rect: CGRect) -> Path {
base.path(in: rect)
}
static func == (
lhs: AnyPaletteShape,
rhs: AnyPaletteShape
) -> Bool {
lhs.typeID == rhs.typeID && lhs.key == rhs.key
}
func hash(into hasher: inout Hasher) {
hasher.combine(typeID)
hasher.combine(key)
}
}
func makePalette() -> Set<AnyPaletteShape> {
var result: Set<AnyPaletteShape> = [
AnyPaletteShape(Disk(label: "Disk")),
AnyPaletteShape(Tile(label: "Tile"))
]
let duplicate = result.insert(
AnyPaletteShape(Disk(label: "Disk"))
)
precondition(!duplicate.inserted)
precondition(result.count == 2)
precondition(result.contains(
AnyPaletteShape(Tile(label: "Tile"))
))
let sameLabel: Set<AnyPaletteShape> = [
AnyPaletteShape(Disk(label: "Sample")),
AnyPaletteShape(Tile(label: "Sample"))
]
precondition(sameLabel.count == 2)
return result
}
struct ContentView: View {
private let shapes = makePalette()
var body: some View {
VStack(alignment: .leading, spacing: 16) {
Text("Shape Palette")
.font(.title)
ForEach(
shapes.sorted { $0.label < $1.label },
id: \.self
) { item in
HStack(spacing: 16) {
GeometryReader { geometry in
item.path(in: CGRect(
origin: .zero,
size: geometry.size
))
.fill(Color.blue)
}
.frame(width: 56, height: 56)
Text(item.label)
}
}
}
.padding(24)
.frame(minWidth: 300, minHeight: 220)
}
}
@main
struct ShapePaletteApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
PaletteShape erft het daadwerkelijke SwiftUI-protocol Shape. De schijf en de tegel implementeren path(in:), de tekenvereiste die Apple documenteert. Hun implementaties gebruiken gedocumenteerde Path-bewerkingen om een ellips en een rechthoek te maken. Door het vierkante weergavekader lijkt de voorbeeldellips rond. Shape, Path.
De initializer met de protocolbeperking is de toegangspoort: init<S: PaletteShape> accepteert alleen typen die aan het gedeelde protocol voldoen. De opgeslagen eigenschappen zijn privé, zodat aanroepers de bewaarde shape of de gelijkheidssleutel niet onafhankelijk van elkaar kunnen vervangen. AnyPaletteShape("Disk") zou een conformancefout tijdens het compileren moeten opleveren, omdat String niet aan PaletteShape voldoet; laat deze opzettelijke afwijzing buiten het uitvoerbare bestand.
Hoewel de set één concreet elementtype heeft, kan elke wrapper een andere concrete shape bewaren. De leden label en path(in:) geven de bewerkingen door die dit palet nodig heeft. De wrapper zelf declareert Hashable; hij declareert geen Shape.
Hoe controleer je invoeging, lidmaatschap en weergavevolgorde?
Voer de app uit in een debugbuild. De opgenomen controles drukken drie verwachte resultaten uit: het invoegen van een tweede gelijke schijf geeft inserted als false terug, een gelijkwaardige tegel wordt gevonden bij de lidmaatschapscontrole en verschillende componenttypen blijven behouden, zelfs als hun labels overeenkomen. Apple documenteert dat insert(_:) zowel een invoegvlag als het resulterende lid teruggeeft. Set.
Het verwachte venster bevat twee rijen, Disk gevolgd door Tile, met blauwe afbeeldingen. Sorteren is een afzonderlijke weergavebewerking, omdat een set geen vastgelegde elementvolgorde heeft. De resulterende array voldoet ook aan de vereiste van ForEach voor een random-access-verzameling; id: \.self gebruikt de hashable waarde van de wrapper als rij-identificatie. Set, ForEach.
Deze volgorde volstaat voor de opgegeven waarden omdat hun labels verschillen. Als toekomstige componenten hetzelfde label hebben, voeg dan een deterministisch secundair sorteerveld toe. Als rijen bewerkbaar worden en hun identiteit moeten behouden wanneer labels veranderen, introduceer dan een stabiele identificatie en beslis afzonderlijk of die identificatie deel uitmaakt van de gelijkheid binnen de set.
Verwacht niet dat het invoegen in een set de bestaande payload vervangt van een component die als gelijk wordt beschouwd. Als vervangen de bedoelde bewerking is, biedt Apple update(with:). Dit onderscheid is van belang als een later model presentatie-eigenschappen uitsluit van de gelijkheid: het invoegen van een gelijke waarde kan het bestaande lid behouden in plaats van de weergave ervan te vernieuwen. Set.
Waar liggen de grenzen van de wrapper?
Hashable-opslag behoudt niet alle SwiftUI-mogelijkheden
De wrapper geeft padgeneratie door voor een statisch palet. Hij geeft de animatable data van de onderliggende shape, inset-bewerkingen of elk ander lid dat van View wordt geërfd niet door. Het tekenen van het geretourneerde pad maakt dit dus nog geen shape-type-uitwissing die animaties behoudt. SwiftUI’s Shape erft ook Animatable en View; door één tekenmethode door te geven, wordt dat volledige contract niet opnieuw opgebouwd. Shape.
Als een andere API een concrete type-uitgewiste shape nodig heeft, biedt SwiftUI AnyShape, beschikbaar in macOS 13 en later. De gedocumenteerde functie ervan is het uitwissen van het concrete type van een shape; Hashable staat niet tussen de opgesomde conformances. Het kan een rendergrens oplossen, maar biedt niet het gelijkheidsbeleid of het aan een protocol gebonden set-element dat we hier hebben gebouwd. AnyShape.
Nieuwe compilerondersteuning heft de beperking voor set-elementen niet op
SE-0352, geïmplementeerd in Swift 5.7, maakt het mogelijk om het onderliggende type van een existential te openen voor geschikte generieke aanroepen. De huidige tekst vermeldt ook gedrag voor de Swift 6-taalmodus. Een waarde openen voor een aanroep maakt any PaletteShape zelf niet tot een Hashable-type dat geschikt is als elementparameter van de set. Behoud de concrete wrapper wanneer je dit ontwerp met die compilerversies gebruikt. Impliciet geopende existentials.
Voor uitbreiding van het model is een keuze over gelijkheid nodig
Het voorbeeld gebruikt onveranderlijke structuren. Beslis bij het toevoegen van afmetingen of andere configuratie die wordt opgeslagen of veranderingen daarin een afzonderlijk setlid opleveren. De automatisch gesynthetiseerde hashing omvat aanvullende hashable opgeslagen eigenschappen. Stem bij aangepaste gelijkheid de hashing precies af op dezelfde essentiële onderdelen, in dezelfde volgorde; overeenkomende hashwaarden stellen op zichzelf nooit gelijkheid vast. Hashable.
Als de applicatie een permanent vaste set componentvarianten heeft, is een enum met hashable geassocieerde waarden een ander redelijk ontwerp: daarmee centraliseer je de ondersteunde gevallen. De wrapper is nuttig wanneer extra componenttypen die het protocol implementeren via dezelfde initializer moeten kunnen worden toegevoegd zonder een centrale lijst van gevallen uit te breiden. Die keuze hangt af van de uitbreidingsbehoeften van de applicatie.
Voor dit Mac-palet geeft de implementatie elke vereiste een concrete plek: het protocol definieert welke shapes worden geaccepteerd, de initializer dwingt de toegang af, gelijkheid bepaalt de duplicaten en het doorgeven van paden levert de weergave. Voeg nieuwe onveranderlijke componenten die het protocol implementeren toe door ze bij invoeging te wrappen, en breid de controles uit telkens wanneer de betekenis van een duplicaat verandert.
Bronnen
- Set — Apple
- AnyHashable — Apple
- Hashable — Apple
- ObjectIdentifier — Apple
- Shape — Apple
- Path — Apple
- ForEach — Apple
- AnyShape — Apple
- Existentials ontgrendelen voor alle protocollen — Swift Evolution
- Impliciet geopende existentials — Swift Evolution