Damit Elemente beim Filtern einer SwiftUI-Sammlung erscheinen und verschwinden, muss die Identität jedes Elements stabil bleiben. Weise jeder Zeile eine Transition zu und animiere Änderungen an den sichtbaren IDs. Dieses Tutorial erstellt ein kleines Mac-Fenster mit lokalen, synthetischen Bezeichnungen und einem funktionierenden Filter.
Die Aufgabe für Leser ist konkret: Gib etwas in ein Filterfeld ein oder ändere einen Kategorie-Schalter. Passende Zeilen sollen daraufhin erscheinen, unpassende verschwinden und die übrigen an ihre neuen Positionen rücken. Apple beschreibt Einfüge- und Entfernungs-Transitionen als Teil einer animierten Zustandsänderung. Die folgende Implementierung überträgt dieses Modell auf eine Sammlung. Animationen
Was sorgt dafür, dass das Filtern korrekt animiert wird?
Drei getrennte Aufgaben greifen ineinander. Der Filter bestimmt, welche Datensätze in der angezeigten Sammlung enthalten sind. Die Identität teilt SwiftUI mit, welche Datensätze bei Aktualisierungen erhalten bleiben. Die Transition beschreibt, wie eine Zeile erscheint oder verschwindet; die Animation legt das Timing fest. Eine Transition allein sorgt noch nicht für eine animierte Zustandsänderung. ForEach, Animationen
Gib jedem Datensatz eine gespeicherte ID, die sich beim Filtern nicht ändert. Das Swift-Protokoll Identifiable steht für eine stabile Objektidentität; ForEach akzeptiert identifizierbare Elemente oder einen expliziten Identitätsschlüsselpfad. In diesem Beispiel kennzeichnen feste Ganzzahlen vier synthetische Datensätze. Identifiable, ForEach
Vermeide es, gefilterte Zeilen anhand ihrer aktuellen Position im Array zu identifizieren. Wird ein früherer Datensatz entfernt, ändern sich die Positionen der nachfolgenden Datensätze, obwohl es weiterhin dieselben Elemente sind. Ebenso verhindert eine berechnete ID, die bei jedem Zugriff eine neue UUID erzeugt, die Kontinuität. Apple veranschaulicht beide Probleme in seiner Erklärung der Identität in SwiftUI. Demystify SwiftUI
Verwende für dieses kleine Beispiel einen normalen VStack in einem ScrollView. Apple empfiehlt bei einer kleinen Anzahl von Unteransichten einen normalen Stack: Er rendert sie gemeinsam, während ein Lazy Stack Ansichten nach Bedarf erstellt. So lässt sich ein Beispiel mit vier Zeilen leichter nachvollziehen, bevor verzögertes Rendering ins Spiel kommt. VStack, ScrollView
Das vollständige Mac-Beispiel erstellen
Erstelle in Xcode ein macOS-App-Projekt mit SwiftUI und Swift und lege für dieses Tutorial das Deployment-Ziel auf macOS 13 oder höher fest. Ersetze die generierte App-Einstiegsdatei durch das vollständige Programm unten. Das Ziel darf genau eine @main-Deklaration enthalten; laut Apples App-Dokumentation gibt es einen einzigen Einstiegspunkt. App
Das Programm verwendet nur sein eigenes Fenster und vier lokale Bezeichnungen. Es enthält das Modell, Beispieldaten, Filtersteuerelemente, die Darstellung der Zeilen, den Animationstrigger und den Leerzustand. Der Code wurde anhand der abgerufenen Dokumentation geprüft, hier aber nicht kompiliert oder ausgeführt, da in der verfügbaren Umgebung keine Swift-Toolchain vorhanden ist.
import SwiftUI
@main
struct FilteredCollectionDemoApp: App {
var body: some Scene {
WindowGroup {
FilteredCollectionView()
}
}
}
private struct DemoItem: Identifiable {
let id: Int
let title: String
let isTile: Bool
}
private struct FilteredCollectionView: View {
@Environment(\.accessibilityReduceMotion)
private var reduceMotion
@State private var query = ""
@State private var tilesOnly = false
private let items: [DemoItem] = [
DemoItem(id: 1, title: "Tile A", isTile: true),
DemoItem(id: 2, title: "Dot A", isTile: false),
DemoItem(id: 3, title: "Tile B", isTile: true),
DemoItem(id: 4, title: "Dot B", isTile: false)
]
private var visibleItems: [DemoItem] {
let needle = query.lowercased()
return items.filter { item in
let matchesText = needle.isEmpty
|| item.title.lowercased().contains(needle)
let matchesCategory = !tilesOnly || item.isTile
return matchesText && matchesCategory
}
}
private var rowTransition: AnyTransition {
.asymmetric(
insertion: AnyTransition.offset(x: 0, y: 8)
.combined(with: .opacity),
removal: .opacity
)
}
var body: some View {
let displayedItems = visibleItems
let displayedIDs = displayedItems.map(\.id)
VStack(alignment: .leading, spacing: 12) {
Text("Label Collection")
.font(.title2)
TextField("Filter labels", text: $query)
.textFieldStyle(.roundedBorder)
Toggle("Tiles only", isOn: $tilesOnly)
Text("\(displayedItems.count) of \(items.count) labels")
.font(.caption)
ScrollView {
VStack(alignment: .leading, spacing: 8) {
ForEach(displayedItems) { item in
HStack {
Text(item.title)
Spacer()
Text(item.isTile ? "Tile" : "Dot")
.foregroundStyle(.secondary)
}
.padding(12)
.frame(maxWidth: .infinity, alignment: .leading)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(Color.blue.opacity(0.10))
)
.transition(rowTransition)
}
if displayedItems.isEmpty {
Text("No matching labels")
.foregroundStyle(.secondary)
.padding(12)
.transition(.opacity)
}
}
.padding(12)
.animation(
reduceMotion
? nil
: .easeInOut(duration: 0.22),
value: displayedIDs
)
}
.frame(height: 300)
}
.padding(20)
.frame(minWidth: 360)
}
}
Der App-Einstiegspunkt zeigt die Sammlung über WindowGroup an, Apples Szenencontainer für Fenster mit identischer Struktur. Wird aus dieser Gruppe ein weiteres Fenster geöffnet, erhält dessen Ansichtshierarchie einen unabhängigen Zustand, einschließlich der Filtereinstellungen in diesem Beispiel. WindowGroup
Der Filter führt einen einfachen, nicht zwischen Groß- und Kleinschreibung unterscheidenden Teilzeichenfolgenvergleich durch, indem er beide Zeichenfolgen in Kleinbuchstaben umwandelt. Für diese synthetischen englischen Bezeichnungen ist das eine bewusste Entscheidung. Die beiden Bedingungen werden kombiniert: Ein Datensatz muss dem Text entsprechen und, falls aktiviert, auch dem Kategorie-Schalter. Das ursprüngliche Array bleibt erhalten, sodass das Zurücksetzen der Steuerelemente alle vier Datensätze wiederherstellt.
Warum die sichtbaren IDs statt der Anzahl der Ergebnisse beobachten?
Der Animationsmodifikator beobachtet displayedIDs, ein geordnetes Array stabiler IDs. Apples animation(_:value:) wendet die angegebene Animation an, wenn sich der überwachte, vergleichbare Wert ändert. Hier steht dieser Wert für die Zugehörigkeit und Reihenfolge der angezeigten Sammlung. animation(_:value:)
Würde nur die Anzahl beobachtet, bliebe ein wichtiger Fall unberücksichtigt: Wenn die Abfrage von a zu b geändert wird, werden zwei Zeilen durch zwei andere ersetzt. Die Anzahl bleibt bei zwei, aber die IDs ändern sich von [1, 2] zu [3, 4]. Die Beobachtung der IDs erkennt auch eine geänderte Reihenfolge, falls später eine Sortierung hinzukommt. Umgekehrt löst eine Abfrageänderung, bei der dieselben geordneten IDs sichtbar bleiben, diesen Modifikator nicht aus.
Der Modifikator sitzt auf dem dauerhaften inneren Stack, der sowohl die Zeilen als auch die Meldung für den Leerzustand enthält. Das Textfeld, der Schalter und die Ergebnisanzahl liegen außerhalb dieses Bereichs. So bleiben die Filtersteuerelemente von der Sammlungsanimation getrennt, und Einfüge- sowie Entfernungsvorgänge haben einen gemeinsamen animierten Vorfahren.
Jede vollständige Zeile erhält ihre Transition nach dem Hinzufügen von Innenabstand und Hintergrundgestaltung. Beim Einfügen werden ein kurzer vertikaler Versatz und Deckkraft kombiniert; beim Entfernen wird nur die Deckkraft verwendet. Apple dokumentiert combined(with:) zum Anwenden beider Transitionen und asymmetric(insertion:removal:) zur Auswahl unterschiedlicher Transitionen für die beiden Richtungen. AnyTransition, asymmetric(insertion:removal:)
Die Beispieldauer beträgt 0,22 Sekunden. Das ist eine Abstimmungsentscheidung, kein dokumentierter Optimalwert. Mit easeInOut(duration:) wird das beabsichtigte Timing explizit angegeben; Apple definiert den Parameter für die Dauer in Sekunden. easeInOut(duration:)
Bei einem filtergesteuerten Button besteht eine weitere unterstützte Möglichkeit darin, den Filterzustand innerhalb von withAnimation zu ändern. Wähle diesen Ansatz, wenn die Aktion selbst die animierte Aktualisierung bestimmen soll. Der wertbasierte Modifikator ist hier praktisch, weil beide Steuerelemente in dieselbe abgeleitete Sammlung einfließen. Apple dokumentiert beide Ansätze. Animationen
Das Verhalten mit einer vorhersehbaren Filterfolge prüfen
Führe die folgende Abfolge in der laufenden App aus. Die erwarteten ID-Arrays ergeben sich aus den Beispieldaten und dem Filterprädikat; sie sind keine berichteten Ausführungsergebnisse.
| Aktion | Erwartete sichtbare IDs | Worauf achten? |
|---|---|---|
| Mit leerem Feld und ausgeschaltetem Schalter beginnen | [1, 2, 3, 4] |
Alle vier Bezeichnungen sind vorhanden. |
a eingeben |
[1, 2] |
Die beiden Bezeichnungen, die auf B enden, verschwinden. |
a durch b ersetzen |
[3, 4] |
Die Zugehörigkeit ändert sich, obwohl die Anzahl bei zwei bleibt. |
Das Feld leeren und dann Tiles only aktivieren |
[1, 3] |
Punktzeilen verschwinden; Kachelzeilen bleiben. |
z eingeben |
[] |
Die übrigen Zeilen verschwinden und No matching labels wird angezeigt. |
| Das Feld leeren und den Schalter deaktivieren | [1, 2, 3, 4] |
Alle Datensätze erscheinen wieder in ihrer ursprünglichen Reihenfolge. |
Das Beispiel liest accessibilityReduceMotion aus und übergibt nil statt einer Animation, wenn diese Einstellung aktiviert ist. Laut Apple sollten Benutzeroberflächen große Animationen vermeiden, wenn dieser Wert true ist; die Animation dieser Sammlung zu deaktivieren, ist die Implementierungsentscheidung in diesem Beispiel. Der Animationsmodifikator dokumentiert, dass eine nil-Animation die Ansicht nicht animiert. accessibilityReduceMotion, animation(_:value:)
Fehlende oder unerwartete Transitionen diagnostizieren
Prüfe diese Ebenen der Reihe nach, damit jede Beobachtung die mögliche Ursache eingrenzt:
- Die gefilterten IDs prüfen. Vergewissere dich, dass das Prädikat die erwarteten Datensätze liefert, bevor du die Animationseinstellungen anpasst.
- Stabilität und Eindeutigkeit der Identität prüfen. Ein beibehaltener Datensatz muss seine ID behalten, und verschiedene Datensätze benötigen unterschiedliche IDs. Entferne berechnete zufällige IDs und positionsbasierte IDs aus einer sich ändernden Sammlung.
- Den Animationstrigger prüfen. Bestätige, dass sich der überwachte Wert bei der getesteten Aktualisierung ändert. Gleiche Anzahlen bedeuten nicht, dass auch die Zugehörigkeit gleich ist.
- Die Platzierung der Transition prüfen. Weise die Transition der vollständigen, von
ForEacherstellten Zeile zu, nicht nur ihrem inneren Text. - Den Animationsbereich prüfen. Der übergeordnete Stack muss bei Filteränderungen bestehen bleiben. Vermeide es, seine Identität von der Abfrage abhängig zu machen.
- Überschreibungen prüfen. Überprüfe die Einstellung
Reduce Motionsowie alle umgebenden Transaktions- oder Animationskonfigurationen, bevor du davon ausgehst, dass die Transition nicht funktioniert.
Die Identitätsprüfungen folgen Apples dokumentierten Anforderungen an stabile, eindeutige IDs. Die übrigen Prüfungen übertragen Apples Unterscheidung zwischen animierten Zustandsänderungen und Einfüge- oder Entfernungstransitionen auf die Ansichtsstruktur dieses Beispiels. Demystify SwiftUI, Animationen
Stabile Datensatz-IDs bewahren den lokalen Ansichtsstatus einer entfernten Zeile nicht unbegrenzt. Apple erklärt, dass das Entfernen einer Ansicht deren Lebensdauer beendet. Muss eine bearbeitbare Zeile einen Entwurf behalten, nachdem sie herausgefiltert wurde, speichere diesen Entwurf außerhalb der Zeile und ordne ihn der Datensatz-ID zu. Dieses Beispiel umgeht das Problem, indem es unveränderliche Bezeichnungen anzeigt. Demystify SwiftUI
Wann solltest du Lazy Stacks, AppKit oder TextKit verwenden?
Ziehe bei größeren Sammlungen einen LazyVStack in Betracht, der untergeordnete Ansichten nur bei Bedarf für die Anzeige erstellt. Filtere die Daten weiterhin, bevor du sie an ForEach übergibst; so erhältst du eine Zeile pro übergebenem Element. Apples ForEach-Dokumentation erklärt, dass Lazy Container besonders nützlich sind, wenn jedes Element eine konstante Anzahl von Ansichten erzeugt. Prüfe Scrollen und Transitionen erneut, nachdem du den Container gewechselt hast. LazyVStack, ForEach
Eine bestehende AppKit-Anwendung kann diese SwiftUI-Oberfläche mit NSHostingView einbetten. Dieser wird als AppKit-Ansicht dokumentiert, die eine SwiftUI-Hierarchie hostet. Identität, Filterung und Transitionen der Sammlung können in ihrer SwiftUI-Stammansicht verbleiben. Durch das Hosting wird die Oberfläche in die Ansichtshierarchie der App integriert. NSHostingView
TextKit dient einer anderen Aufgabe: der Verwaltung von Textspeicherung und benutzerdefiniertem Textlayout, unter anderem über AppKits NSTextView. Dieses Beispiel zum Filtern von Bezeichnungen benötigt keine eigene Textlayout-Engine. Führe TextKit ein, wenn die Anforderungen an Textbearbeitung oder Layout in der Zeile es erfordern; die Zugehörigkeit zur Sammlung und die Zeilenübergänge bleiben in der umgebenden SwiftUI-Oberfläche. TextKit
Beginne mit der Abfolge aus vier Datensätzen und bestätige Identität und Zugehörigkeit, bevor du die Bewegung abstimmst. Sobald der Austausch bei gleicher Anzahl und der Fall ohne Ergebnisse wie vorgesehen funktionieren, kannst du das Zeilendesign und die Datenquelle anpassen. Dabei sollten die IDs stabil bleiben und der Animationstrigger an die angezeigte Sammlung gebunden sein.
Quellen
- Animationen — Apple
- ForEach — Apple
- Identifiable — Apple
- Demystify SwiftUI — Apple
- VStack — Apple
- ScrollView — Apple
- App — Apple
- WindowGroup — Apple
- animation(_:value:) — Apple
- AnyTransition — Apple
- asymmetric(insertion:removal:) — Apple
- easeInOut(duration:) — Apple
- accessibilityReduceMotion — Apple
- LazyVStack — Apple
- NSHostingView — Apple
- TextKit — Apple