Um verschiedene SwiftUI-Shapes in einem Set zu speichern und dabei eine gemeinsame Schnittstelle durchzusetzen, verwende einen konkreten Hashable-Wrapper mit einem protokollbeschränkten Initializer. In diesem Tutorial entsteht eine kleine Shape-Palette für den Mac mit expliziten Regeln für Duplikate und aufrufbaren Shape-Operationen.
Die Aufgabe ist klar umrissen: synthetische Scheiben- und Kachelkomponenten sammeln, nicht zugehörige Werte zur Compile-Zeit ablehnen, Duplikate entfernen und die verbleibenden Komponenten in einem SwiftUI-Fenster anzeigen. Die Sammlung nimmt über ein gemeinsames Protokoll, das SwiftUIs Shape verfeinert, mehrere Komponententypen auf.
Warum erfüllt ein Set aus Protokollwerten diese Anforderungen nicht?
Swifts Set verlangt, dass sein Elementtyp Hashable entspricht. Die Deklaration, dass jeder Wert innerhalb eines Existenzials Hashable entspricht, verleiht diesem Existenzialtyp keine eigene Konformität. Daher wird Set<any PaletteShape> nicht allein dadurch gültig, dass PaletteShape von Hashable erbt. Apple dokumentiert die Elementbeschränkung der Sammlung, während Swift Evolution den Unterschied zwischen einem Existenzialcontainer und dem darin enthaltenen konformen Wert erklärt. Set, Implicitly Opened Existentials.
Ein Existenzial wie any PaletteShape kann verschiedene konforme Typen enthalten. Die Schwierigkeit liegt beim Gleichheitsvergleich: Die geerbte Anforderung von Equatable vergleicht zwei Werte desselben Self-Typs, während zwei Existenzialcontainer unterschiedliche konkrete Typen enthalten können. Swifts erweiterte Unterstützung für Existenzialwerte stellt keine universelle Gleichheitsoperation für solche Container bereit. Unlock existentials for all protocols.
Diese Alternativen lösen jeweils unterschiedliche Teile des Problems:
| Repräsentation | Gemischte konkrete Typen? | Gemeinsame Schnittstelle durchgesetzt? | Als Set-Element geeignet? |
|---|---|---|---|
[any PaletteShape] |
Ja | Ja | Ein Array gewährleistet keine Eindeutigkeit der Elemente |
Set<S> mit S: PaletteShape |
Ein ausgewählter S |
Ja | Ja, für diesen einzelnen konkreten Typ |
Set<AnyHashable> |
Ja | Keine Shape-Beschränkung | Ja |
Set<AnyPaletteShape> weiter unten |
Ja, innerhalb von Wrappern | Ja, bei der Konstruktion | Ja |
AnyHashable stellt typgelöschte Gleichheit und Hashing bereit, sein Initializer akzeptiert aber generell hashbare Werte. Ein Set, das nur daraus besteht, könnte daher auch einen String oder eine Ganzzahl aufnehmen. Der benutzerdefinierte Wrapper weiter unten beschränkt die Aufnahme auf das Shape-Protokoll der Anwendung und behält neben dem typgelöschten hashbaren Wert auch die Shape-Schnittstelle bei. AnyHashable.
Was soll als doppeltes Shape gelten?
Lege die Gleichheitsregeln fest, bevor du den Wrapper implementierst. In dieser Palette sind zwei Komponenten Duplikate, wenn sie denselben konkreten Komponententyp und gleiche gespeicherte Werte haben. Eine Scheibe mit der Beschriftung Sample und eine Kachel mit der Beschriftung Sample bleiben separate Einträge. Zwei Scheiben mit derselben Beschriftung werden zu einem Eintrag zusammengefasst.
Die Beschriftung ist die einzige gespeicherte Eigenschaft jeder Beispielkomponente, daher beziehen sich synthetisierte Gleichheit und Hashing auf sie. Das ist eine illustrative Modellentscheidung: Visuelle Ähnlichkeit allein definiert keine Gleichheit. Apple dokumentiert die automatische Hashable-Synthese für Strukturen, deren gespeicherte Eigenschaften hashbar sind, und verlangt, dass gleiche Werte dieselben Komponenten in ihre Hasher einspeisen. Hashable.
Der Wrapper nimmt außerdem eine Typkennung in Gleichheit und Hashing auf. Damit wird die Regel „gleicher Typ“ explizit, statt Annahmen über typgelöschte Vergleiche vorauszusetzen. AnyHashable kann bestimmte Werte mit unterschiedlichen zugrunde liegenden Typen anhand kanonischer Repräsentationen vergleichen; Apple definiert es nicht als uneingeschränkten Unterscheider konkreter Typen. ObjectIdentifier unterstützt Metatypen und ermöglicht diesem Beispiel dadurch, den generischen Eingabetyp zu identifizieren. AnyHashable, ObjectIdentifier.
Das vollständige Mac-Palettenbeispiel erstellen
Verwende in Xcode ein macOS-SwiftUI-App-Projekt mit einem Swift-5.7-Compiler oder neuer. Wähle als reproduzierbare Ausgangsbasis den Swift-5-Sprachmodus und ein macOS-Bereitstellungsziel von 13.0 oder neuer. Das Bereitstellungsziel ist eine konservative Wahl für dieses Tutorial und bedeutet nicht, dass jede der folgenden APIs macOS 13 erfordert.
Die Mindestversion des Compilers ist relevant, weil das Beispiel any PaletteShape speichert, dessen geerbte Protokolle Anforderungen mit Associated Types und Self umfassen. SE-0309 hält fest, dass diese erweiterte Unterstützung für Existenziale in Swift 5.7 implementiert wurde. Das Beispiel benötigt keine neuere Funktion für verallgemeinerte Existenzialkonformität. Unlock existentials for all protocols.
Ersetze die generierte App-Deklaration und Content-View durch die folgende vollständige Quelldatei. Im App-Target darf nur eine @main-Deklaration vorhanden sein. Das Beispiel enthält die Komponentendefinitionen, den Wrapper, Beispielwerte, Prüfungen, die View und den App-Einstiegspunkt. Swift ist in der Rechercheumgebung nicht verfügbar, daher wurde dieser Code geprüft, aber hier nicht kompiliert oder ausgeführt.
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 erbt von SwiftUIs tatsächlichem Shape-Protokoll. Scheibe und Kachel stellen path(in:) bereit, die von Apple dokumentierte Zeichenanforderung. Ihre Implementierungen verwenden dokumentierte Path-Operationen, um eine Ellipse und ein Rechteck zu erzeugen. Durch den quadratischen Anzeigerahmen erscheint die Beispiel-Ellipse kreisförmig. Shape, Path.
Der beschränkte Initializer ist die Aufnahmeregel: init<S: PaletteShape> akzeptiert nur einen Typ, der dem gemeinsamen Protokoll entspricht. Die gespeicherten Eigenschaften sind privat, sodass Aufrufer die beibehaltene Shape oder ihren Gleichheitsschlüssel nicht unabhängig voneinander ersetzen können. AnyPaletteShape("Disk") sollte einen Konformitätsfehler zur Compile-Zeit auslösen, da String nicht PaletteShape entspricht; nimm diese absichtliche Ablehnung nicht in die ausführbare Datei auf.
Obwohl das Set einen einzigen konkreten Elementtyp hat, kann jeder Wrapper ein anderes konkretes Shape speichern. Seine Member label und path(in:) leiten die für diese Palette benötigten Operationen weiter. Der Wrapper selbst deklariert Hashable; er deklariert nicht Shape.
Wie überprüfst du Einfügen, Zugehörigkeit und Anzeigereihenfolge?
Führe die App in einem Debug-Build aus. Die enthaltenen Prüfungen formulieren drei erwartete Ergebnisse: Beim Einfügen einer zweiten gleichen Scheibe ist inserted false, die Zugehörigkeitsprüfung findet eine gleiche Kachel, und unterschiedliche Komponententypen bleiben erhalten, selbst wenn ihre Beschriftungen übereinstimmen. Apple dokumentiert, dass insert(_:) sowohl ein Einfügekennzeichen als auch das resultierende Element zurückgibt. Set.
Das erwartete Fenster enthält zwei Zeilen, Disk gefolgt von Tile, mit blauer Grafik. Die Sortierung ist ein separater Anzeigeschritt, da für ein Set keine Elementreihenfolge festgelegt ist. Das resultierende Array erfüllt außerdem die Anforderung von ForEach an eine Random-Access-Collection; id: \.self verwendet den hashbaren Wert des Wrappers als Zeilenkennung. Set, ForEach.
Diese Sortierung reicht für die angegebenen Werte aus, weil sich ihre Beschriftungen unterscheiden. Wenn künftige Komponenten gemeinsame Beschriftungen haben, ergänze ein deterministisches sekundäres Sortierfeld. Wenn Zeilen bearbeitbar werden und ihre Identität trotz geänderter Beschriftungen beibehalten müssen, führe eine stabile Kennung ein und entscheide separat, ob diese Kennung Teil der Set-Gleichheit sein soll.
Erwarte nicht, dass das Einfügen in ein Set die Nutzdaten einer bereits vorhandenen gleichen Komponente ersetzt. Für einen Austausch stellt Apple update(with:) bereit. Dieser Unterschied ist wichtig, wenn ein späteres Modell Darstellungseigenschaften von der Gleichheit ausschließt: Beim Einfügen eines gleichen Werts kann das vorhandene Element erhalten bleiben, statt seine Darstellung zu aktualisieren. Set.
Wo liegen die Grenzen des Wrappers?
Hashable-Speicherung erhält nicht alle SwiftUI-Funktionen
Der Wrapper leitet die Pfaderzeugung für eine statische Palette weiter. Er leitet weder die animierbaren Daten des zugrunde liegenden Shapes noch Inset-Operationen oder jedes von View geerbte Element weiter. Das Zeichnen des zurückgegebenen Pfads ergibt daher keinen Shape-Typeraser, der Animationen erhält. SwiftUIs Shape erbt außerdem Animatable und View; die Implementierung einer einzigen weitergeleiteten Zeichenmethode stellt nicht den gesamten Vertrag wieder her. Shape.
Wenn eine andere API ein konkretes, typgelöschtes Shape benötigt, stellt SwiftUI AnyShape bereit, verfügbar ab macOS 13. Seine dokumentierte Rolle ist es, den konkreten Typ eines Shapes zu löschen; unter seinen aufgeführten Konformitäten ist Hashable nicht enthalten. Es kann eine Rendering-Grenze abdecken, stellt aber weder die Gleichheitsrichtlinie noch das hier erstellte protokollbeschränkte Set-Element bereit. AnyShape.
Neue Compilerfunktionen heben die Elementbeschränkung des Sets nicht auf
SE-0352, implementiert in Swift 5.7, erlaubt es, den zugrunde liegenden Typ eines Existenzials für geeignete generische Aufrufe zu öffnen. Der aktuelle Text dokumentiert außerdem das Verhalten im Swift-6-Sprachmodus. Einen Wert für einen Aufruf zu öffnen, macht any PaletteShape nicht selbst zu einem Hashable-Typ, der sich als Elementparameter des Sets eignet. Verwende bei diesem Entwurf über diese Compiler-Generationen hinweg den konkreten Wrapper. Implicitly Opened Existentials.
Eine Erweiterung des Modells erfordert eine Entscheidung zur Gleichheit
Das Beispiel verwendet unveränderliche Strukturen. Wenn du Abmessungen oder andere gespeicherte Konfiguration hinzufügst, entscheide, ob Änderungen daran ein separates Set-Element ergeben. Synthetisiertes Hashing bezieht zusätzliche hashbare gespeicherte Eigenschaften ein. Bei benutzerdefinierter Gleichheit müssen genau dieselben wesentlichen Komponenten in derselben Reihenfolge gehasht werden; übereinstimmende Hashwerte allein begründen niemals Gleichheit. Hashable.
Wenn die Anwendung dauerhaft eine feste Menge von Komponentenvarianten hat, ist auch ein Enum mit hashbaren Associated Values ein sinnvoller Entwurf: Es bündelt die unterstützten Fälle an einer Stelle. Der Wrapper eignet sich, wenn zusätzliche konforme Komponententypen über denselben Initializer aufgenommen werden sollen, ohne eine zentrale Fallliste zu erweitern. Diese Wahl richtet sich nach dem Erweiterungsbedarf der Anwendung.
Für diese Mac-Palette erhält jede Anforderung einen konkreten Platz: Das Protokoll definiert die akzeptierten Shapes, der Initializer setzt die Aufnahmeregel durch, die Gleichheit definiert Duplikate, und die Pfadweiterleitung ermöglicht das Rendern. Füge neue unveränderliche konforme Komponenten hinzu, indem du sie beim Einfügen umhüllst, und erweitere die Prüfungen immer dann, wenn sich die Bedeutung eines Duplikats ändert.
Quellen
- Set — Apple
- AnyHashable — Apple
- Hashable — Apple
- ObjectIdentifier — Apple
- Shape — Apple
- Path — Apple
- ForEach — Apple
- AnyShape — Apple
- Unlock existentials for all protocols — Swift Evolution
- Implicitly Opened Existentials — Swift Evolution