Aby animować pojawianie się i znikanie elementów w filtrowanej kolekcji SwiftUI, zachowaj stałą tożsamość każdego elementu, przypisz przejście do każdego wiersza i animuj zmiany widocznych identyfikatorów. W tym samouczku zbudujemy małe okno Maca z lokalnymi, syntetycznymi etykietami i działającym filtrem.
Zadanie czytelnika jest konkretne: wpisz tekst w pole filtra lub zmień przełącznik kategorii, a następnie obserwuj, jak pasujące wiersze się pojawiają, niepasujące znikają, a pozostałe ustawiają się w nowych pozycjach. Apple opisuje przejścia wstawiania i usuwania jako część animowanej zmiany stanu. Poniższa implementacja stosuje ten model do kolekcji. Animacje
Co sprawia, że filtrowanie jest poprawnie animowane?
Współdziałają tu trzy odrębne elementy. Filtr określa, które rekordy należą do wyświetlanej kolekcji. Tożsamość informuje SwiftUI, które rekordy pozostają te same między aktualizacjami. Przejście opisuje sposób pojawiania się lub znikania wiersza, a animacja określa czas. Samo dodanie przejścia nie powoduje animowanej zmiany stanu. ForEach, Animacje
Przypisz każdemu rekordowi zapisany identyfikator, który nie zmienia się podczas filtrowania. Protokół Swift Identifiable reprezentuje stałą tożsamość obiektu; ForEach przyjmuje elementy identyfikowalne albo jawną ścieżkę klucza tożsamości. W tym przykładzie cztery syntetyczne rekordy identyfikują stałe liczby całkowite. Identifiable, ForEach
Nie identyfikuj filtrowanych wierszy na podstawie ich bieżących pozycji w tablicy. Usunięcie wcześniejszego rekordu zmienia późniejsze pozycje, mimo że odpowiadające im elementy pozostają te same. Podobnie obliczany identyfikator, który przy każdym odczycie generuje nowy UUID, niszczy ciągłość. Apple opisuje oba problemy w wyjaśnieniu tożsamości w SwiftUI. Demystify SwiftUI
W tym niewielkim przykładzie użyj zwykłego VStack wewnątrz ScrollView. Apple zaleca zwykły stos dla niewielkiej liczby widoków podrzędnych; renderuje je razem, podczas gdy leniwy stos tworzy widoki w razie potrzeby. Dzięki temu łatwiej przeanalizować przykład z czterema wierszami, zanim wprowadzisz renderowanie odroczone. VStack, ScrollView
Zbuduj kompletny przykład dla Maca
Utwórz w Xcode projekt aplikacji macOS, używając SwiftUI i Swift, a jego minimalną wersję systemu ustaw na macOS 13 lub nowszą. Zastąp wygenerowany plik wejściowy aplikacji poniższym kompletnym programem. W obiekcie docelowym pozostaw dokładnie jedną deklarację @main; dokumentacja Apple dotycząca App określa pojedynczy punkt wejścia. App
Program korzysta wyłącznie z własnego okna i czterech lokalnych etykiet. Zawiera model, przykładowe dane, kontrolki filtra, widok wiersza, wyzwalacz animacji i stan pusty. Kod sprawdzono pod kątem zgodności z pobraną dokumentacją, ale nie został tutaj skompilowany ani uruchomiony, ponieważ w dostępowym środowisku nie ma narzędzi Swift.
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)
}
}
Punkt wejścia aplikacji wyświetla kolekcję za pomocą WindowGroup, kontenera scen Apple przeznaczonego dla identycznie zbudowanych okien. Jeśli z tej grupy otworzy się kolejne okno, SwiftUI nada jego hierarchii widoków niezależny stan, w tym ustawienia filtra z tego przykładu. WindowGroup
Filtr wykonuje proste, niewrażliwe na wielkość liter porównanie podciągów, zamieniając oba ciągi na małe litery. To celowy wybór dla tych syntetycznych etykiet angielskich. Oba warunki są łączone: rekord musi pasować do tekstu oraz, jeśli przełącznik jest włączony, do wybranej kategorii. Oryginalna tablica pozostaje bez zmian, więc wyczyszczenie kontrolek przywraca wszystkie cztery rekordy.
Dlaczego obserwować widoczne identyfikatory zamiast liczby wyników?
Modyfikator animacji obserwuje displayedIDs, czyli uporządkowaną tablicę stałych identyfikatorów. Apple opisuje animation(_:value:) jako mechanizm stosujący podaną animację, gdy monitorowana wartość zgodna z Equatable ulegnie zmianie. Tutaj ta wartość reprezentuje skład i kolejność wyświetlanej kolekcji. animation(_:value:)
Obserwowanie tylko liczby elementów pomija istotny przypadek: zmiana zapytania z a na b zastępuje dwa wiersze dwoma innymi. Liczba nadal wynosi dwa, ale identyfikatory zmieniają się z [1, 2] na [3, 4]. Obserwowanie identyfikatorów wykryje też zmianę kolejności rekordów, jeśli później dodasz sortowanie. Z kolei zmiana zapytania, która pozostawia widoczne te same identyfikatory w tej samej kolejności, nie uruchamia tego modyfikatora.
Modyfikator znajduje się na trwałym stosie wewnętrznym, który zawiera zarówno wiersze, jak i komunikat o pustym stanie. Pole tekstowe, przełącznik i liczba wyników leżą poza jego zakresem. Dzięki temu kontrolki filtra są oddzielone od animacji kolekcji, a wspólny przodek wierszy obsługuje animowane zmiany wstawiania i usuwania.
Każdy kompletny wiersz otrzymuje przejście po zastosowaniu dopełnienia i stylu tła. Pojawianie się łączy krótkie przesunięcie w pionie z przezroczystością; znikanie wykorzystuje samą przezroczystość. Apple opisuje combined(with:) jako łączenie obu przejść, a asymmetric(insertion:removal:) jako wybór różnych przejść dla obu kierunków. AnyTransition, asymmetric(insertion:removal:)
Przykładowy czas trwania to 0.22 sekundy. To wartość do dostrojenia, a nie udokumentowane optimum. Podanie easeInOut(duration:) jawnie określa zamierzony czas; Apple definiuje parametr czasu trwania w sekundach. easeInOut(duration:)
W przypadku filtra uruchamianego przyciskiem można też zmieniać stan filtra wewnątrz withAnimation. Wybierz to rozwiązanie, gdy sama akcja ma określać animowaną aktualizację. Modyfikator oparty na wartości jest tu wygodny, ponieważ obie kontrolki wpływają na tę samą pochodną kolekcję. Apple opisuje oba podejścia. Animacje
Sprawdź działanie za pomocą przewidywalnej sekwencji filtrowania
W uruchomionej aplikacji wykonaj poniższą sekwencję. Oczekiwane tablice identyfikatorów wynikają z przykładowych danych i predykatu filtra; nie są wynikami wykonania programu.
| Działanie | Oczekiwane widoczne identyfikatory | Co sprawdzić |
|---|---|---|
| Zacznij od pustego pola i wyłączonego przełącznika | [1, 2, 3, 4] |
Widoczne są wszystkie cztery etykiety. |
Wpisz a |
[1, 2] |
Znikają dwie etykiety kończące się na B. |
Zastąp a przez b |
[3, 4] |
Zmienia się skład, mimo że liczba elementów pozostaje równa dwa. |
Wyczyść pole, a następnie włącz Tylko kafelki |
[1, 3] |
Wiersze z kropką znikają, a wiersze z kafelkami pozostają. |
Wpisz z |
[] |
Pozostałe wiersze znikają i pojawia się Nie znaleziono pasujących etykiet. |
| Wyczyść pole i wyłącz przełącznik | [1, 2, 3, 4] |
Wszystkie rekordy wracają w pierwotnej kolejności. |
Przykład odczytuje accessibilityReduceMotion i zamiast animacji przekazuje nil, gdy ta preferencja jest włączona. Apple zaleca unikanie dużych animacji interfejsu, gdy ta wartość jest prawdziwa; wyłączenie animacji tej kolekcji jest wyborem implementacyjnym zastosowanym w przykładzie. Dokumentacja modyfikatora animacji określa, że animacja nil nie animuje widoku. accessibilityReduceMotion, animation(_:value:)
Diagnozuj brakujące lub nieoczekiwane przejścia
Sprawdzaj te warstwy po kolei, aby każda obserwacja zawężała przyczynę:
- Sprawdź filtrowane identyfikatory. Zanim zmienisz ustawienia animacji, upewnij się, że predykat zwraca oczekiwane rekordy.
- Sprawdź stałość i unikalność tożsamości. Zachowany rekord musi zachować swój identyfikator, a różne rekordy muszą mieć różne identyfikatory. Usuń z modyfikującej się kolekcji losowe identyfikatory obliczane na bieżąco oraz identyfikatory oparte na pozycji.
- Sprawdź wyzwalacz animacji. Upewnij się, że monitorowana wartość zmienia się w aktualizacji, którą testujesz. Taka sama liczba elementów nie oznacza tego samego składu.
- Sprawdź położenie przejścia. Przypisz przejście do całego wiersza tworzonego przez
ForEach, a nie tylko do zawartego w nim tekstu. - Sprawdź zakres animacji. Zachowaj stos nadrzędny podczas zmian filtra. Nie uzależniaj jego tożsamości od zapytania.
- Sprawdź nadpisania. Zanim uznasz, że przejście nie działa, sprawdź ustawienie
Ogranicz ruchoraz otaczającą transakcję lub konfigurację animacji.
Kontrole tożsamości wynikają z udokumentowanych przez Apple wymagań dotyczących stałych i unikalnych identyfikatorów. Pozostałe kontrole stosują do struktury widoków tego przykładu opisany przez Apple rozdział między animowanymi aktualizacjami stanu a przejściami wstawiania i usuwania. Demystify SwiftUI, Animacje
Stałe identyfikatory rekordów nie zachowują w nieskończoność lokalnego stanu widoku usuniętego wiersza. Apple wyjaśnia, że usunięcie widoku kończy jego cykl życia. Jeśli edytowalny wiersz ma zachować wersję roboczą po odfiltrowaniu, przechowuj ją poza wierszem i powiąż z identyfikatorem rekordu. Ten przykład unika tego problemu, wyświetlając niezmienne etykiety. Demystify SwiftUI
Kiedy używać leniwych stosów, AppKit lub TextKit?
W przypadku większych kolekcji rozważ użycie LazyVStack, który tworzy widoki podrzędne tylko wtedy, gdy są potrzebne do wyświetlenia. Filtruj dane przed przekazaniem ich do ForEach; dzięki temu powstanie po jednym wierszu dla każdego dostarczonego elementu. Dokumentacja Apple dotycząca ForEach wyjaśnia, że kontenery leniwe zyskują na wydajności, gdy każdy element tworzy stałą liczbę widoków. Po zmianie kontenerów ponownie sprawdź przewijanie i przejścia. LazyVStack, ForEach
Istniejącą aplikację AppKit można zintegrować z tym interfejsem SwiftUI za pomocą NSHostingView, opisanego jako widok AppKit osadzający hierarchię SwiftUI. Tożsamość elementów kolekcji, filtrowanie i przejścia mogą pozostać w jej głównym widoku SwiftUI. Osadzenie integruje interfejs z hierarchią widoków samej aplikacji. NSHostingView
TextKit służy innemu zadaniu: zarządzaniu pamięcią tekstu i niestandardowym układem tekstu, między innymi za pomocą AppKitowego NSTextView. Ten przykład filtrowania etykiet nie wymaga własnego silnika układu tekstu. Wprowadź TextKit, gdy wymagania dotyczące edycji lub układu tekstu wiersza tego wymagają; zarządzanie składem kolekcji i przejściami wierszy pozostaw otaczającemu interfejsowi SwiftUI. TextKit
Zacznij od sekwencji z czterema rekordami i potwierdź tożsamość oraz skład kolekcji, zanim dostroisz ruch. Gdy przypadek zastąpienia przy tej samej liczbie elementów oraz przypadek pustego wyniku będą działać zgodnie z oczekiwaniami, dostosuj projekt wiersza i źródło danych, zachowując stałe identyfikatory i powiązując wyzwalacz animacji z wyświetlaną kolekcją.
Źródła
- Animacje — 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