För att animera objekt som kommer in och går ur en filtrerad SwiftUI-samling, behåll varje objekts identitet stabil, koppla en övergång till varje rad och animera ändringar i de synliga ID:n. Den här handledningen bygger ett litet Mac-fönster med lokala syntetiska etiketter och ett fungerande filter.

Uppgiften för läsaren är specifik: skriv i ett filterfält eller ändra en kategoriväxel, se sedan matchande rader komma in, icke-matchande rader gå och de återstående raderna placera sig i sina nya positioner. Apple dokumenterar infognings- och borttagningsövergångar som en del av en animerad tillståndsändring. Implementeringen nedan applicerar den modellen på en samling. Animations

Vad gör att filtrering animeras korrekt?

Tre separata ansvarsområden arbetar tillsammans. Filtret avgör vilka poster som tillhör den visade samlingen. Identitet säger till SwiftUI vilka poster som fortsätter över uppdateringar. Övergången beskriver hur en rad visas eller försvinner, medan animationen levererar tidtagning. Att lägga till en övergång ensam etablerar ingen animerad tillståndsändring. ForEach, Animations

Ge varje post ett lagrat identifierare som förblir oförändrad vid filtrering. Swifts Identifiable-protokoll representerar stabil entitetsidentitet; ForEach accepterar identifierbara element eller en explicit identitetsnyckelväg. För det här exemplet identifierar fasta heltal fyra syntetiska poster. Identifiable, ForEach

Undvik att identifiera filtrerade rader efter deras nuvarande arraypositioner. Att ta bort en tidigare post ändrar senare positioner, även om dessa poster fortfarande är samma objekt. Därigenom förstör en beräknad identifierare som genererar ett nytt UUID vid varje åtkomst kontinuiteten. Apple demonstrerar båda problemen i sin förklaring av SwiftUI-identitet. Demystify SwiftUI

Använd en vanlig VStack inuti en ScrollView för det här lilla exemplet. Apple rekommenderar en vanlig stack för ett litet antal underuvyer; den renderar dem tillsammans, medan en lat stack skapar vyer vid behov. Det gör att ett fyr-rad-exempel blir enklare att inspektera innan man introducerar uppskjuten rendering. VStack, ScrollView

Bygg det kompletta Mac-exemplet

Skapa ett macOS-appprojekt i Xcode med SwiftUI och Swift, och sätt dess distributionsmål till macOS 13 eller senare för den här handledningen. Ersätt den genererade appens startfil med det kompletta programmet nedan. Behåll exakt en @main-deklaration i målet; Apples App-dokumentation specificerar en enda startpunkt. App

Programmet använder bara sitt eget fönster och fyra lokala etiketter. Den innehåller modellen, exempeldata, filterkontroller, radpresentation, animationsutlösare och tomtillstånd. Koden granskades mot den hämtade dokumentationen men kompilerades eller kördes inte här eftersom den tillgängliga miljön saknar Swift-verktygskedja.

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)
    }
}

Appens startpunkt presenterar samlingen genom WindowGroup, Apples scencontainer för identiskt strukturerade fönster. Om ett annat fönster öppnas från den gruppen ger SwiftUI dess vyhierarki oberoende tillstånd, inklusive det här exemplets filterinställningar. WindowGroup

Filtret utför en enkel skiftlägesokänslig delsträngjämförelse genom att sätta båda strängarna i gemener. Det är ett avsiktligt val för dessa syntetiska engelska etiketter. De två villkoren kombineras: en post måste matcha texten och, när det är aktiverat, kategoriväxeln. Den ursprungliga arrayen förblir intakt, så att man genom att rensa kontrollerna återställer alla fyra posterna.

Varför övervaka de synliga ID:n istället för antalet resultat?

Animationsmodifieraren övervakar displayedIDs, en ordnad array av stabila identifierare. Apples animation(_:value:) applicerar den tillhandahållna animationen när dess övervakade, likvärdiga värde ändras. Här representerar det värdet medlemskap och ordning i den visade samlingen. animation(_:value:)

Att bara övervaka antalet skulle missa ett användbart fall: att ändra frågan från a till b byter ut två rader mot två andra rader. Antalet förblir två, men ID:n ändras från [1, 2] till [3, 4]. Att övervaka ID:n upptäcker också omordnade poster om sortering läggs till senare. Däremot utlöser inte en frågeändring som lämnar samma ordnade ID:n synliga den här modifieraren.

Modifieraren sitter på den beständiga inre stacken som innehåller både raderna och tomtillståndsmeddelandet. Textfältet, växeln och resultatantalet sitter utanför det omfånget. Denna arrangement håller filterkontrollerna separata från samlingens animation och ger infognings- och borttagningsändringar en gemensam animerad förfader.

Varje komplett rad får sin övergång efter utfyllnad och bakgrundsstyling. Infogningen kombinerar ett kort vertikalt offset med opacitet; borttagning använder enbart opacitet. Apple dokumenterar combined(with:) som applicerar båda övergångarna och asymmetric(insertion:removal:) som väljer olika övergångar för de två riktningarna. AnyTransition, asymmetric(insertion:removal:)

Den illustrativa varaktigheten är 0,22 sekunder. Det är ett justeringsval, inte ett dokumenterat optimum. Att specificera easeInOut(duration:) gör den avsedda tidtagningen explicit; Apple definierar dess duration-parameter i sekunder. easeInOut(duration:)

För ett knapptrivent filter är ett annat stödt tillvägagångssätt att ändra filtertillståndet inuti withAnimation. Välj det när själva åtgärden ska definiera den animerade uppdateringen. Värdebaserade modifieraren är bekväm här eftersom båda kontrollerna matar samma härledda samling. Apple dokumenterar båda tillvägagångssätten. Animations

Kontrollera beteendet med en förutsägbar filtersekvens

Använd följande sekvens i den körande appen. De förväntade ID-arrayerna härleds från exempeldatan och filterpredikatet; de är inte rapporterade exekveringsresultat.

Åtgärd Förväntade synliga ID Vad att inspektera
Starta med tomt fält och växel av [1, 2, 3, 4] Alla fyra etiketter är närvarande.
Ange a [1, 2] De två etiketter som slutar på B går.
Ersätt a med b [3, 4] Medlemskap ändras även om antalet förblir två.
Rensa fältet, aktivera sedan Tiles only [1, 3] Punktrader går; panelerader stannar.
Ange z [] Återstående rader går och Inga matchande etiketter visas.
Rensa fältet och inaktivera växeln [1, 2, 3, 4] Alla poster återvänder i sin ursprungliga ordning.

Exemplet läser accessibilityReduceMotion och levererar nil istället för en animation när den inställningen är aktiverad. Apple säger att gränssnitt ska undvika stora animationer när detta värde är sant; att inaktivera den här samlingens animation är exemplets implementeringsval. Animationsmodifieraren dokumenterar att en nil animation inte animerar vyn. accessibilityReduceMotion, animation(_:value:)

Diagnostisera saknade eller oväntade övergångar

Kontrollera dessa lager i ordning så att varje observation begränsar orsaken:

  1. Inspektera de filtrerade ID:n. Verifiera att predicatet producerar de förväntade posterna innan du justerar animationsinställningar.
  2. Inspektera identitetsstabilitet och unikhet. En behållen post måste behålla sin identifierare, och olika poster behöver olika identifierare. Ta bort beräknade slumpmässiga ID:n och positionsbaserade ID:n från en föränderlig samling.
  3. Inspektera animationsutlösaren. Bekräfta att det övervakade värdet ändras för den uppdatering som testas. Lika antal innebär inte lika medlemskap.
  4. Inspektera övergångsplacering. Koppla övergången till den fullständiga raden som skapas av ForEach, snarare än bara till dess inre text.
  5. Inspektera det animerade omfånget. Håll förälderstacken närvarande över filterändringar. Undvik att göra dess identitet beroende av frågan.
  6. Inspektera åsidosättningar. Kontrollera Reduce Motion-inställningen och eventuell omgivande transaktion eller animationskonfiguration innan du drar slutsatsen att övergången misslyckas.

Identitetskontrollerna följer Apples dokumenterade krav på stabila, unika identifierare. De övriga kontrollerna applicerar Apples separation mellan animerade tillståndsuppdateringar och infognings- eller borttagningsövergångar på det här exempelns vystruktur. Demystify SwiftUI, Animations

Stabila post-ID:n bevarar inte en borttagen rads lokala vytillstånd outändligt. Apple förklarar att att ta bort en vy avslutar dess livstid. Om en redigerbar rad måste behålla ett utkast efter att ha filtrerats bort, lagra det utkastet utanför raden, associerat med post-ID:t. Det här exemplet undviker det problemet genom att visa oförändliga etiketter. Demystify SwiftUI

När ska du använda lata stackar, AppKit eller TextKit?

För större samlingar, överväg en LazyVStack, som skapar barn endast vid behov för visning. Behåll filtrering av datan innan du skickar den till ForEach; detta ger en rad per tillfört element. Apples ForEach-dokumentation förklarar att lata containrar gynnas när varje element producerar ett konstant antal vyer. Kontrollera omrullning och övergångar igen efter att ha bytt containrar. LazyVStack, ForEach

En befintlig AppKit-applikation kan bädda in det här SwiftUI-gränssnittet med NSHostingView, dokumenterad som en AppKit-vy som värd för en SwiftUI-hierarki. Samlingens identitet, filtrering och övergångar kan förbli i dess SwiftUI-rotvy. Värdintegrering integrerar gränssnittet i appens egen vyhierarki. NSHostingView

TextKit tjänar en annan uppgift: hantera textlagring och anpassad textlayout, inklusive genom AppKits NSTextView. Inget i det här etikettfiltreringsexemplet kräver en anpassad textlayoutmotor. Introducera TextKit när radens textredigering eller layoutkrav kräver det; håll medlemskap i samlingen och radövergångar i det omgivande SwiftUI-gränssnittet. TextKit

Börja med den fyr-posts-sekvensen och bekräfta identitet och medlemskap innan du justerar rörelse. När fall med samma antal men ersättning och tomt resultat beter sig som avsett, anpassa raddesignen och datakällan med hållna identifierare stabla och animationsutlösaren knuten till den visade samlingen.

Källor