Om du vill lagra olika SwiftUI-former i en enda mängd och samtidigt upprätthålla ett gemensamt gränssnitt använder du en konkret Hashable-omslagstyp med en protokollbegränsad initializerare. Den här handledningen bygger en liten formpalett för Mac med uttryckliga regler för dubbletter och anropbara formoperationer.

Uppgiften är specifik: samla syntetiska disk- och plattkomponenter, avvisa orelaterade värden vid kompilering, ta bort dubbletter och visa de återstående komponenterna i ett SwiftUI-fönster. Samlingen accepterar flera komponenttyper genom ett gemensamt protokoll som utökar SwiftUIs Shape.

Varför uppfyller inte en mängd protokollvärden det här kontraktet?

Swifts Set kräver att elementtypen följer Hashable. Att deklarera att varje värde i en existential följer Hashable innebär inte att den existentialtypen får sin egen konformans. Därför blir Set<any PaletteShape> inte giltig bara för att PaletteShape ärver Hashable. Apple dokumenterar samlingens elementbegränsning, medan Swift Evolution förklarar skillnaden mellan en existentialbehållare och dess underliggande konforma värde. Set, Implicitly Opened Existentials.

En existential som any PaletteShape kan innehålla olika konforma typer. Svårigheten gäller likhet: det ärvda kravet Equatable jämför två värden av samma Self-typ, medan två existentialbehållare kan innehålla olika konkreta typer. Swifts utökade stöd för existentialvärden tillhandahåller inte någon universell likhetsoperation för dessa behållare. Unlock existentials for all protocols.

De här alternativen löser olika delar av problemet:

Representation Blandade konkreta typer? Upprätthålls ett gemensamt gränssnitt? Lämplig elementtyp för en mängd?
[any PaletteShape] Ja Ja En array tillhandahåller inte mängdens unika element
Set<S> med S: PaletteShape En vald S Ja Ja, för den enda konkreta typen
Set<AnyHashable> Ja Inget formkrav Ja
Set<AnyPaletteShape> nedan Ja, inuti omslagstyper Ja, vid konstruktion Ja

AnyHashable tillhandahåller typ-erad likhet och hashning, men dess initializerare accepterar i allmänhet hashbara värden. En mängd som enbart innehåller den typen skulle därför kunna acceptera en sträng eller ett heltal. Den anpassade omslagstypen nedan begränsar indata till programmets formprotokoll och behåller formgränssnittet tillsammans med det typ-erade hashbara värdet. AnyHashable.

Vad ska räknas som en dubblettform?

Bestäm likheten innan du implementerar omslagstypen. I den här paletten är två komponenter dubbletter när de har samma konkreta komponenttyp och lika lagrade värden. En disk med etiketten Sample och en platta med etiketten Sample förblir separata poster. Två diskar med samma etikett slås ihop till en.

Etiketten är den enda lagrade egenskapen i varje exempelkomponent, så den syntetiserade likheten och hashningen omfattar den. Det här är ett illustrativt modellval: enbart visuell likhet definierar inte likhet. Apple dokumenterar automatisk syntes av Hashable för strukturer vars lagrade egenskaper är hashbara, och kräver att lika värden skickar samma komponenter till sina hashare. Hashable.

Omslagstypen inkluderar också en typidentifierare i både likheten och hashningen. Det gör regeln om samma typ uttrycklig, i stället för att förlita sig på antaganden om jämförelser efter typ-erasure. AnyHashable kan jämföra vissa värden av olika underliggande typer med kanoniska representationer; Apple definierar den inte som en ovillkorlig skiljare mellan konkreta typer. ObjectIdentifier stöder metatyper, vilket låter det här exemplet identifiera den generiska indatatypen. AnyHashable, ObjectIdentifier.

Bygg det fullständiga palettexemplet för Mac

Använd ett SwiftUI-approjekt för macOS i Xcode med en kompilator för Swift 5.7 eller senare. Välj Swift 5-språkläge och ett distributionsmål för macOS 13.0 eller senare för en reproducerbar grundkonfiguration. Distributionsmålet är ett försiktigt val för den här handledningen, inte ett påstående om att alla API:er nedan kräver macOS 13.

Kompilatorns lägsta version är viktig eftersom exemplet lagrar any PaletteShape, vars ärvda protokoll omfattar krav på associerade typer och Self. SE-0309 anger att det här utökade stödet för existentialer implementerades i Swift 5.7. Exemplet kräver inte någon nyare funktion för generaliserad existentialkonformans. Unlock existentials for all protocols.

Ersätt den genererade appdeklarationen och innehållsvyn med följande fullständiga källfil. Behåll bara en @main-deklaration i appmålet. Exemplet innehåller komponentdefinitioner, omslagstyp, exempelvärden, kontroller, vy och appens startpunkt. Swift är inte tillgängligt i forskningsmiljön, så koden granskades men kompilerades eller kördes inte här.

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 ärver SwiftUIs faktiska Shape-protokoll. Disken och plattan tillhandahåller path(in:), ritkravet som Apple dokumenterar. Deras implementationer använder dokumenterade Path-operationer för att skapa en ellips och en rektangel. Den kvadratiska visningsramen gör att exempel-ellipsen ser cirkelformad ut. Shape, Path.

Den begränsade initializeraren är inmatningsregeln: init<S: PaletteShape> accepterar bara en typ som följer det gemensamma protokollet. De lagrade egenskaperna är privata, så anropare kan inte byta ut den behållna formen eller dess likhetsnyckel oberoende av varandra. AnyPaletteShape("Disk") bör ge ett konformansfel vid kompilering eftersom String inte följer PaletteShape; ta bort det avsiktliga avvisningsfallet från den körbara filen.

Även om mängden har en enda konkret elementtyp kan varje omslagstyp behålla en annan konkret form. Medlemmarna label och path(in:) vidarebefordrar de operationer som paletten behöver. Själva omslagstypen deklarerar Hashable; den deklarerar inte Shape.

Hur verifierar du insättning, medlemskap och visningsordning?

Kör appen i en felsökningsversion. Kontrollerna som ingår uttrycker tre förväntade resultat: att infoga en andra likadan disk ger värdet inserted som false, att medlemskapskontrollen hittar en likadan platta och att olika komponenttyper finns kvar även när deras etiketter matchar. Apple dokumenterar att insert(_:) returnerar både en insättningsflagga och det resulterande elementet. Set.

Det förväntade fönstret innehåller två rader, Disk följt av Tile, med blå grafik. Sortering är en separat visningsoperation eftersom en mängd inte har någon definierad elementordning. Den resulterande arrayen uppfyller också ForEach-kravet på en samling med direktåtkomst; id: \.self använder omslagstypens hashbara värde som radidentifierare. Set, ForEach.

Den här ordningen räcker för de angivna värdena eftersom deras etiketter skiljer sig åt. Om framtida komponenter delar etiketter lägger du till ett deterministiskt sekundärt sorteringsfält. Om raderna blir redigerbara och måste behålla sin identitet när etiketterna ändras inför du en stabil identifierare och beslutar separat om identifieraren ska ingå i mängdens likhet.

Förvänta dig inte att en insättning i mängden ersätter en befintlig komponent med samma värde. När ersättning är den avsedda åtgärden tillhandahåller Apple update(with:). Skillnaden spelar roll om en senare modell utesluter presentations egenskaper från likheten: då kan en insättning av ett likvärdigt värde behålla det befintliga elementet i stället för att uppdatera dess utseende. Set.

Var går omslagstypens gränser?

Hashbar lagring bevarar inte alla SwiftUI-funktioner

Omslagstypen vidarebefordrar sökvägsgenerering för en statisk palett. Den vidarebefordrar inte den underliggande formens animerbara data, inåtgående operationer eller alla medlemmar som ärvts från View. Att rita den returnerade sökvägen innebär därför inte att det är en typ-erasure av former som bevarar animering. SwiftUIs Shape ärver också Animatable och View; att implementera en vidarebefordrad ritmetod återskapar inte hela det kontraktet. Shape.

Om ett annat API behöver en konkret typ-eraderad form tillhandahåller SwiftUI AnyShape, som finns i macOS 13 och senare. Dess dokumenterade roll är att typ-eradera en forms konkreta typ, och dess listade konformanser omfattar inte Hashable. Den kan lösa en gräns för rendering, men den tillhandahåller inte likhetspolicyn eller den protokollbegränsade elementtypen för en mängd som byggs här. AnyShape.

Nya kompilatorfunktioner tar inte bort mängdens elementkrav

SE-0352, som implementerades i Swift 5.7, låter en existentials underliggande typ öppnas för lämpliga generiska anrop. Den aktuella texten beskriver också beteendet i Swift 6-språkläget. Att öppna ett värde för ett anrop gör inte any PaletteShape till en Hashable-typ som passar som mängdens elementparameter. Behåll den konkreta omslagstypen när du använder den här designen genom dessa kompilatorgenerationer. Implicitly Opened Existentials.

En utökning av modellen kräver ett likhetsbeslut

Exemplet använder oföränderliga strukturer. När du lägger till dimensioner eller annan lagrad konfiguration behöver du avgöra om ändringar ska skapa en separat medlem i mängden. Syntetiserad hashning omfattar ytterligare hashbara lagrade egenskaper. För anpassad likhet ska du hasha exakt samma väsentliga komponenter i samma ordning; matchande hashvärden fastställer aldrig i sig likhet. Hashable.

Om programmet har en permanent fast uppsättning komponentvarianter är en enum med hashbara associerade värden ett annat rimligt val: den samlar de stödda fallen på ett ställe. Omslagstypen är användbar när ytterligare konforma komponenttyper ska kunna läggas till genom samma initializerare utan att en central lista med fall behöver utökas. Det valet beror på programmets behov av utbyggnad.

För den här Mac-paletten får varje krav en konkret plats i implementationen: protokollet definierar vilka former som accepteras, initializeraren styr vad som får läggas till, likheten definierar dubbletter och vidarebefordran av sökvägen möjliggör rendering. Lägg till nya oföränderliga komponenter som följer protokollet genom att omsluta dem vid insättning, och utöka sedan kontrollerna varje gång betydelsen av en dubblett ändras.

Källor