Als je items wilt animeren die een gefilterde SwiftUI-collectie binnenkomen of verlaten, houd dan de identiteit van elk item stabiel, koppel een transitie aan elke rij en animeer wijzigingen in de zichtbare ID’s. In deze tutorial bouwen we een klein Mac-venster met lokale synthetische labels en een werkend filter.
De taak voor de lezer is concreet: typ in een filterveld of wijzig een categoriewisselaar en zie vervolgens overeenkomende rijen verschijnen, niet-overeenkomende rijen verdwijnen en de overgebleven rijen naar hun nieuwe posities schuiven. Apple documenteert invoeg- en verwijdertransities als onderdeel van een geanimeerde statuswijziging. De onderstaande implementatie past dat model toe op een collectie. Animations
Waardoor werkt filteren met animaties?
Drie afzonderlijke verantwoordelijkheden werken samen. Het filter bepaalt welke records in de weergegeven collectie thuishoren. Identiteit vertelt SwiftUI welke records bij updates behouden blijven. De transitie beschrijft hoe een rij verschijnt of verdwijnt, terwijl de animatie de timing bepaalt. Alleen een transitie toevoegen zorgt niet voor een geanimeerde statuswijziging. ForEach, Animations
Geef elk record een opgeslagen ID die bij het filteren ongewijzigd blijft. Het Swift-protocol Identifiable staat voor de stabiele identiteit van een entiteit; ForEach accepteert identificeerbare elementen of een expliciet pad naar de identiteitskey. In dit voorbeeld identificeren vaste gehele getallen vier synthetische records. Identifiable, ForEach
Gebruik de huidige posities in de array niet als identiteit voor gefilterde rijen. Als een eerder record wordt verwijderd, veranderen de posities van latere records, ook al zijn het nog steeds dezelfde items. Een berekende ID die bij elke toegang een nieuwe UUID genereert, verbreekt eveneens de continuïteit. Apple laat beide problemen zien in de uitleg over SwiftUI-identiteit. Demystify SwiftUI
Gebruik voor dit kleine voorbeeld een gewone VStack in een ScrollView. Apple raadt een gewone stack aan voor een klein aantal subviews; die rendert ze allemaal tegelijk, terwijl een lazy stack views aanmaakt wanneer dat nodig is. Daardoor is een voorbeeld met vier rijen makkelijker te inspecteren voordat je uitgestelde rendering introduceert. VStack, ScrollView
Bouw het complete Mac-voorbeeld
Maak in Xcode een macOS-appproject met SwiftUI en Swift en stel voor deze tutorial de implementatiedoelversie in op macOS 13 of hoger. Vervang het gegenereerde app-entrybestand door het volledige programma hieronder. Zorg dat het target precies één declaratie van @main bevat; in Apples documentatie van App staat dat er één entrypoint is. App
Het programma gebruikt alleen het eigen venster en vier lokale labels. Het bevat het model, voorbeeldgegevens, filterbediening, rijweergave, animatietrigger en lege status. De code is gecontroleerd aan de hand van de opgehaalde documentatie, maar is hier niet gecompileerd of uitgevoerd omdat de beschikbare omgeving geen Swift-toolchain heeft.
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)
}
}
Het app-entrypoint toont de collectie via WindowGroup, Apples scenecontainer voor vensters met dezelfde structuur. Als vanuit die groep een ander venster wordt geopend, geeft SwiftUI de viewhiërarchie daarvan een eigen status, waaronder de filterinstellingen van dit voorbeeld. WindowGroup
Het filter voert een eenvoudige hoofdletterongevoelige substringvergelijking uit door beide strings om te zetten naar kleine letters. Dat is een bewuste keuze voor deze synthetische Engelse labels. De twee voorwaarden worden gecombineerd: een record moet overeenkomen met de tekst en, wanneer ingeschakeld, met de categoriewisselaar. De oorspronkelijke array blijft intact, zodat het wissen van de bedieningselementen alle vier records terugzet.
Waarom de zichtbare ID’s volgen in plaats van het aantal resultaten?
De animatiemodifier volgt displayedIDs, een geordende array van stabiele ID’s. Apples animation(_:value:) past de opgegeven animatie toe wanneer de gevolgde, vergelijkbare waarde verandert. Hier staat die waarde voor het lidmaatschap en de volgorde van de weergegeven collectie. animation(_:value:)
Alleen het aantal volgen zou een nuttig geval missen: als je de zoekopdracht wijzigt van a naar b, worden twee rijen vervangen door twee andere rijen. Het aantal blijft twee, maar de ID’s veranderen van [1, 2] in [3, 4]. Als later sortering wordt toegevoegd, detecteert het volgen van de ID’s ook gewijzigde volgordes. Omgekeerd activeert een wijziging van de zoekopdracht die dezelfde geordende zichtbare ID’s oplevert deze modifier niet.
De modifier staat op de blijvende binnenste stack die zowel de rijen als het bericht voor de lege status bevat. Het tekstveld, de wisselaar en het aantal resultaten vallen buiten dat bereik. Zo blijven de filterbediening en de collectie-animatie van elkaar gescheiden en delen invoeg- en verwijderwijzigingen één geanimeerde bovenliggende view.
Elke volledige rij krijgt na de opvulling en achtergrondopmaak een transitie. De invoegtransitie combineert een kleine verticale verschuiving met dekking; bij verwijderen wordt alleen de dekking gebruikt. Apple documenteert combined(with:) als een combinatie van beide transities en asymmetric(insertion:removal:) als de keuze voor verschillende transities per richting. AnyTransition, asymmetric(insertion:removal:)
De illustratieve duur is 0,22 seconden. Dit is een keuze om af te stellen, geen gedocumenteerde optimale waarde. Door easeInOut(duration:) op te geven, wordt de bedoelde timing expliciet; Apple definieert de parameter voor de duur in seconden. easeInOut(duration:)
Bij een filter dat door een knop wordt aangestuurd, kun je de filterstatus ook binnen withAnimation wijzigen. Kies die aanpak als de actie zelf de geanimeerde update moet bepalen. De modifier op basis van een waarde is hier handig omdat beide bedieningselementen dezelfde afgeleide collectie aansturen. Apple documenteert beide aanpakken. Animations
Controleer het gedrag met een voorspelbare filterreeks
Voer de volgende reeks uit in de actieve app. De verwachte ID-arrays zijn afgeleid van de voorbeeldgegevens en het filterpredicaat; het zijn geen gerapporteerde uitvoeringsresultaten.
| Actie | Verwachte zichtbare ID’s | Waarop letten |
|---|---|---|
| Begin met een leeg veld en de wisselaar uit | [1, 2, 3, 4] |
Alle vier labels zijn aanwezig. |
Voer a in |
[1, 2] |
De twee labels die eindigen op B verdwijnen. |
Vervang a door b |
[3, 4] |
Het lidmaatschap verandert, hoewel het aantal twee blijft. |
Wis het veld en schakel daarna Tiles only in |
[1, 3] |
Puntvormige rijen verdwijnen; tegelrijen blijven staan. |
Voer z in |
[] |
De overgebleven rijen verdwijnen en No matching labels verschijnt. |
| Wis het veld en schakel de wisselaar uit | [1, 2, 3, 4] |
Alle records keren terug in hun oorspronkelijke volgorde. |
Het voorbeeld leest accessibilityReduceMotion en gebruikt nil in plaats van een animatie wanneer die voorkeur is ingeschakeld. Volgens Apple moeten interfaces grote animaties vermijden wanneer deze waarde waar is; de animatie van deze collectie uitschakelen is de implementatiekeuze in dit voorbeeld. De animatiemodifier documenteert dat een animatie van nil de view niet animeert. accessibilityReduceMotion, animation(_:value:)
Ontbrekende of onverwachte transities opsporen
Controleer deze lagen in volgorde, zodat elke waarneming de mogelijke oorzaak verkleint:
- Controleer de gefilterde ID’s. Ga na of het predicaat de verwachte records oplevert voordat je de animatie-instellingen aanpast.
- Controleer de stabiliteit en uniciteit van de identiteit. Een behouden record moet zijn ID behouden en verschillende records moeten verschillende ID’s hebben. Verwijder berekende willekeurige ID’s en op posities gebaseerde ID’s uit een veranderende collectie.
- Controleer de animatietrigger. Bevestig dat de gevolgde waarde verandert bij de update die je test. Een gelijk aantal betekent niet dat het lidmaatschap gelijk is.
- Controleer de plaatsing van de transitie. Koppel de transitie aan de volledige rij die door
ForEachwordt gemaakt, niet alleen aan de tekst erin. - Controleer het geanimeerde bereik. Zorg dat de bovenliggende stack bij filterwijzigingen blijft bestaan. Laat de identiteit ervan niet afhangen van de zoekopdracht.
- Controleer op overschrijvingen. Controleer de instelling
Reduce Motionen eventuele omringende transactie- of animatieconfiguratie voordat je concludeert dat de transitie niet werkt.
De identiteitscontroles volgen de gedocumenteerde vereisten van Apple voor stabiele, unieke ID’s. De overige controles passen Apples onderscheid tussen geanimeerde statusupdates en invoeg- of verwijdertransities toe op de viewstructuur van dit voorbeeld. Demystify SwiftUI, Animations
Stabiele record-ID’s zorgen er niet voor dat de lokale viewstatus van een verwijderde rij behouden blijft. Apple legt uit dat de levensduur van een view eindigt wanneer die wordt verwijderd. Als een bewerkbare rij een concept moet behouden nadat die uit het filter is verdwenen, bewaar dat concept dan buiten de rij en koppel het aan de record-ID. Dit voorbeeld vermijdt dat probleem door onveranderlijke labels weer te geven. Demystify SwiftUI
Wanneer gebruik je lazy stacks, AppKit of TextKit?
Overweeg voor grotere collecties een LazyVStack, die kinderen alleen aanmaakt wanneer ze moeten worden weergegeven. Blijf de gegevens filteren voordat je ze aan ForEach doorgeeft; zo ontstaat één rij per aangeleverd element. In Apples ForEach-documentatie staat dat lazy containers voordeel opleveren wanneer elk element een vast aantal views aanmaakt. Controleer het scrollen en de transities opnieuw nadat je van container bent veranderd. LazyVStack, ForEach
Een bestaande AppKit-applicatie kan deze SwiftUI-interface opnemen met NSHostingView, gedocumenteerd als een AppKit-view die een SwiftUI-hiërarchie host. De identiteit, filtering en transities van de collectie kunnen in de rootview van SwiftUI blijven. Hosting integreert de interface in de viewhiërarchie van de app zelf. NSHostingView
TextKit is bedoeld voor een andere taak: het beheren van tekstopslag en aangepaste tekstopmaak, onder meer via AppKit’s NSTextView. Voor dit voorbeeld met label- en tekstfiltering is geen aangepaste tekstindelingsengine nodig. Introduceer TextKit wanneer de vereisten voor tekstbewerking of tekstopmaak in de rij daarom vragen; houd het lidmaatschap van de collectie en de rijtransities in de omringende SwiftUI-interface. TextKit
Begin met de reeks van vier records en controleer de identiteit en het lidmaatschap voordat je de beweging afstelt. Zodra de vervanging met hetzelfde aantal en de situatie met een leeg resultaat zich gedragen zoals bedoeld, kun je het rijontwerp en de gegevensbron aanpassen, met behoud van stabiele ID’s en een animatietrigger die gekoppeld is aan de weergegeven collectie.
Bronnen
- Animations — 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