Filtrelenmiş bir SwiftUI koleksiyonuna öğelerin girip çıkmasını canlandırmak için her öğenin kimliğini sabit tutun, her satıra bir geçiş ekleyin ve görünür kimliklerdeki değişiklikleri canlandırın. Bu eğitimde yerel, sentetik etiketler ve çalışan bir filtre içeren küçük bir Mac penceresi oluşturuluyor.
Okuyucunun yapmak istediği şey açık: Bir filtre alanına yazın veya kategori anahtarını değiştirin; eşleşen satırların göründüğünü, eşleşmeyenlerin kaybolduğunu ve kalanların yeni konumlarına yerleştiğini görün. Apple, ekleme ve kaldırma geçişlerini animasyonlu bir durum değişikliğinin parçası olarak belgeliyor. Aşağıdaki uygulama bu modeli bir koleksiyona uyarlıyor. Animations
Filtreleme animasyonunun doğru çalışmasını ne sağlar?
Üç ayrı sorumluluk birlikte çalışır. Filtre, hangi kayıtların görüntülenen koleksiyona ait olduğunu belirler. Kimlik, güncellemeler boyunca hangi kayıtların aynı kaldığını SwiftUI’a bildirir. Geçiş, bir satırın nasıl görünüp kaybolacağını tanımlarken animasyon zamanlamayı sağlar. Tek başına geçiş eklemek, animasyonlu bir durum değişikliği oluşturmaz. ForEach, Animations
Filtreleme sırasında değişmeden kalan, saklanan bir tanımlayıcıyı her kayda verin. Swift’in Identifiable protokolü, varlıkların sabit kimliğini temsil eder; ForEach ise tanımlanabilir öğeleri veya açıkça belirtilmiş bir kimlik anahtar yolunu kabul eder. Bu örnekte dört sentetik kaydı tanımlamak için sabit tamsayılar kullanılıyor. Identifiable, ForEach
Filtrelenmiş satırları geçerli dizi konumlarına göre tanımlamaktan kaçının. Önceki bir kaydı kaldırmak, sonraki konumları değiştirir; oysa bu kayıtlar hâlâ aynı öğelerdir. Benzer şekilde, her erişimde yeni bir UUID oluşturan hesaplanmış bir tanımlayıcı sürekliliği bozar. Apple, SwiftUI kimliğini açıkladığı belgede her iki sorunu da gösteriyor. Demystify SwiftUI
Bu küçük örnekte bir ScrollView içinde normal bir VStack kullanın. Apple, az sayıda alt görünüm için normal bir yığın öneriyor; normal yığın bunları birlikte işlerken tembel yığın görünümleri gerektiğinde oluşturur. Böylece ertelenmiş işlemeyi eklemeden önce dört satırlı örneği incelemek kolaylaşır. VStack, ScrollView
Mac için eksiksiz örneği oluşturun
Xcode’da SwiftUI ve Swift kullanan bir macOS uygulama projesi oluşturun ve bu eğitim için dağıtım hedefini macOS 13 veya üzeri olarak ayarlayın. Oluşturulan uygulama giriş dosyasını aşağıdaki programın tamamıyla değiştirin. Hedefte tam olarak bir @main bildirimi bulundurun; Apple’ın App belgelerinde tek bir giriş noktası belirtiliyor. App
Program yalnızca kendi penceresini ve dört yerel etiketi kullanır. Modeli, örnek verileri, filtre kontrollerini, satır görünümünü, animasyon tetikleyicisini ve boş durumu içerir. Kod, erişilen belgelere göre incelendi ancak mevcut ortamda Swift araç zinciri bulunmadığından derlenip çalıştırılmadı.
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)
}
}
Uygulama giriş noktası, Apple’ın aynı yapıya sahip pencereler için sahne kapsayıcısı olan WindowGroup aracılığıyla koleksiyonu sunar. Bu gruptan başka bir pencere açılırsa SwiftUI, bu örneğin filtre ayarları da dahil olmak üzere o pencerenin görünüm hiyerarşisine bağımsız bir durum verir. WindowGroup
Filtre, her iki dizeyi de küçük harfe çevirerek basit, büyük-küçük harfe duyarsız bir alt dize karşılaştırması yapar. Bu, sentetik İngilizce etiketler için bilinçli bir tercihtir. İki koşul birlikte uygulanır: Bir kayıt hem metinle eşleşmeli hem de etkinse kategori anahtarını karşılamalıdır. Özgün dizi değişmeden kalır; böylece kontroller temizlendiğinde dört kaydın tümü geri gelir.
Neden sonuç sayısını değil, görünür kimlikleri izlemeli?
Animasyon değiştiricisi, sıralı ve sabit tanımlayıcılardan oluşan bir dizi olan displayedIDs değerini izler. Apple’ın animation(_:value:) değiştiricisi, izlediği eşitlenebilir değer değiştiğinde sağlanan animasyonu uygular. Burada bu değer, görüntülenen koleksiyonun üyelerini ve sırasını temsil eder. animation(_:value:)
Yalnızca sayıyı izlemek kullanışlı bir durumu gözden kaçırır: Sorguyu a değerinden b değerine değiştirmek, iki satırı başka iki satırla değiştirir. Sayı yine ikidir ancak kimlikler [1, 2] değerinden [3, 4] değerine değişir. Daha sonra sıralama eklenirse kimlikleri izlemek yeniden sıralanan kayıtları da algılar. Buna karşılık, sorgu değişikliği görünür kimliklerin sırasını değiştirmiyorsa bu değiştirici tetiklenmez.
Değiştirici, hem satırları hem de boş durum mesajını içeren kalıcı iç yığına uygulanır. Metin alanı, anahtar ve sonuç sayısı bu kapsamın dışında kalır. Bu düzen, filtre kontrollerini koleksiyon animasyonundan ayrı tutar ve ekleme ile kaldırma değişikliklerine ortak bir animasyonlu üst görünüm sağlar.
Her satırın tamamına, dolgu ve arka plan biçimlendirmesinden sonra geçiş uygulanır. Ekleme geçişi kısa bir dikey kaymayı opaklıkla birleştirir; kaldırmada yalnızca opaklık kullanılır. Apple, combined(with:) öğesinin her iki geçişi de uyguladığını, asymmetric(insertion:removal:) öğesinin ise iki yönde farklı geçişler seçtiğini belgeliyor. AnyTransition, asymmetric(insertion:removal:)
Örnek süre 0.22 saniyedir. Bu, belgelenmiş en iyi değer değil, ince ayar tercihidir. easeInOut(duration:) belirtmek amaçlanan zamanlamayı açık hâle getirir; Apple süre parametresini saniye cinsinden tanımlar. easeInOut(duration:)
Düğmeyle tetiklenen bir filtre için desteklenen başka bir yaklaşım, filtre durumunu withAnimation içinde değiştirmektir. Eylemin kendisinin animasyonlu güncellemeyi belirlemesini istiyorsanız bu yaklaşımı seçin. Burada değer tabanlı değiştirici kullanışlıdır; çünkü her iki kontrol de aynı türetilmiş koleksiyonu etkiler. Apple her iki yaklaşımı da belgeliyor. Animations
Davranışı öngörülebilir bir filtre sırasıyla kontrol edin
Çalışan uygulamada aşağıdaki sırayı izleyin. Beklenen kimlik dizileri, örnek veriler ve filtre koşulundan çıkarılmıştır; çalıştırma sonucu olarak raporlanmamıştır.
| Eylem | Beklenen görünür kimlikler | İncelenecekler |
|---|---|---|
| Boş alanla başlayıp anahtarı kapalı tutun | [1, 2, 3, 4] |
Dört etiketin tümü görünür. |
a girin |
[1, 2] |
B ile biten iki etiket kaybolur. |
a değerini b ile değiştirin |
[3, 4] |
Sayı iki kalmasına rağmen üyelik değişir. |
Alanı temizleyin, ardından Tiles only seçeneğini etkinleştirin |
[1, 3] |
Nokta satırları kaybolur; karo satırları kalır. |
z girin |
[] |
Kalan satırlar kaybolur ve No matching labels görünür. |
| Alanı temizleyip anahtarı kapatın | [1, 2, 3, 4] |
Tüm kayıtlar özgün sıralarıyla geri gelir. |
Örnek, accessibilityReduceMotion değerini okur ve bu tercih etkin olduğunda animasyon yerine nil sağlar. Apple, bu değer doğru olduğunda arayüzlerin büyük animasyonlardan kaçınması gerektiğini belirtiyor; bu koleksiyonun animasyonunu devre dışı bırakmak örneğin uygulama tercihidir. Animasyon değiştiricisinin belgelerine göre nil animasyonu görünümü canlandırmaz. accessibilityReduceMotion, animation(_:value:)
Eksik veya beklenmeyen geçişleri teşhis edin
Her gözlemin nedeni daraltmasına yardımcı olmak için bu katmanları sırayla kontrol edin:
- Filtrelenmiş kimlikleri inceleyin. Animasyon ayarlarını değiştirmeden önce koşulun beklenen kayıtları ürettiğini doğrulayın.
- Kimliklerin sabitliğini ve benzersizliğini inceleyin. Tutulan bir kayıt kimliğini korumalı, farklı kayıtların ise farklı kimlikleri olmalıdır. Değişen koleksiyondan hesaplanan rastgele kimlikleri ve konum tabanlı kimlikleri kaldırın.
- Animasyon tetikleyicisini inceleyin. İzlenen değerin test edilen güncellemede değiştiğini doğrulayın. Sayıların eşit olması, üyeliğin de aynı olduğu anlamına gelmez.
- Geçişin yerleşimini inceleyin. Geçişi yalnızca iç metne değil,
ForEachtarafından oluşturulan satırın tamamına ekleyin. - Animasyon kapsamını inceleyin. Filtre değişiklikleri boyunca üst yığını koruyun. Kimliğini sorguya bağlı hâle getirmeyin.
- Geçersiz kılmaları inceleyin. Geçişin başarısız olduğu sonucuna varmadan önce
Reduce Motionayarını ve çevredeki işlem veya animasyon yapılandırmalarını kontrol edin.
Kimlik kontrolleri, Apple’ın sabit ve benzersiz tanımlayıcılara ilişkin belgelenmiş gereksinimlerini izler. Diğer kontroller, animasyonlu durum güncellemeleri ile ekleme veya kaldırma geçişlerini Apple’ın belgelerinde ayrı ele alışını bu örneğin görünüm yapısına uygular. Demystify SwiftUI, Animations
Sabit kayıt kimlikleri, kaldırılmış bir satırın yerel görünüm durumunu süresiz olarak korumaz. Apple, görünüm kaldırıldığında ömrünün sona erdiğini açıklıyor. Düzenlenebilir bir satırın filtre dışına çıkarıldıktan sonra taslağını koruması gerekiyorsa bu taslağı satırın dışında, kayıt kimliğiyle ilişkilendirilmiş şekilde saklayın. Bu örnek, değiştirilemez etiketler göstererek bu sorunu önlüyor. Demystify SwiftUI
Tembel yığınları, AppKit’i veya TextKit’i ne zaman kullanmalısınız?
Daha büyük koleksiyonlar için çocuk görünümleri yalnızca gerektiğinde oluşturan bir LazyVStack kullanmayı değerlendirin. Verileri ForEach’e göndermeden önce filtrelemeye devam edin; bu, sağlanan her öğe için bir satır oluşturur. Apple’ın ForEach belgelerinde, her öğenin sabit sayıda görünüm oluşturduğu durumlarda tembel kapsayıcıların avantaj sağladığı açıklanıyor. Kapsayıcıyı değiştirdikten sonra kaydırma ve geçişleri yeniden kontrol edin. LazyVStack, ForEach
Mevcut bir AppKit uygulaması, Apple’ın SwiftUI hiyerarşisini barındıran bir AppKit görünümü olarak belgelediği NSHostingView ile bu SwiftUI arayüzünü ekleyebilir. Koleksiyonun kimliği, filtrelemesi ve geçişleri SwiftUI kök görünümünde kalabilir. Barındırma, arayüzü uygulamanın kendi görünüm hiyerarşisine ekler. NSHostingView
TextKit farklı bir göreve hizmet eder: AppKit’in NSTextView öğesi de dahil olmak üzere metin depolamasını ve özel metin düzenini yönetir. Bu etiket filtreleme örneğinde özel bir metin düzeni motoru gerekmiyor. Satırın metin düzenleme veya yerleşim gereksinimleri bunu gerektirdiğinde TextKit’i kullanın; koleksiyon üyeliğini ve satır geçişlerini çevresindeki SwiftUI arayüzünde tutun. TextKit
Dört kayıtlı diziyle başlayın ve hareketi ince ayarlamadan önce kimlikleri ve üyeliği doğrulayın. Aynı sayıda öğenin birbirinin yerine geçmesi ve boş sonuç durumları beklendiği gibi çalıştıktan sonra, kimlikleri sabit tutup animasyon tetikleyicisini görüntülenen koleksiyona bağlayarak satır tasarımını ve veri kaynağını uyarlayın.
Kaynaklar
- 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