Ders 18 / 21
Geçişler
Modelden silinen öğenin liste görünümünü gözlem tetiğiyle, ayrıntı alanını açan durumun ekranı yerel tetikle yeniden hesaplattığının ve kalkan görünümün hesaplanmadığının ölçümü.
Yayın Güncelleme
Bu derste
- Geçişi, bir görünümün ağaca girişi ve çıkışı için seçilen yansıma biçimi olarak tanımlar
- Modelden silinen öğenin listeyi gözlem tetiğiyle yeniden hesaplattığını ölçer
- Gözlem izlemenin yalnız okunan özelliğe tepki verdiğini ölçer
- Kalkan görünümün yeniden hesaplanmadığını, ağaçtan çıktığını gösterir
İçindekiler
Önceki derste animasyonun değişikliği sardığını, tetiği değiştirmediğini ve görünümün ağaçta kaldığını ölçtük. Bu derste görünüm ağaçta kalmaz: ya ağaca girer ya da çıkar. Soru yine aynıdır: bu yeniden hesaplamayı ne tetikledi? Bu derste bakılan tetik gözlem; yanında yerel bir örnek de vardır.
Geçiş, bir görünümün ağaca eklenirken ya da ağaçtan çıkarılırken nasıl yansıyacağını seçen bildirimsel bir tanımdır. Görünüm, bir koşula bağlı olarak gövdede yer alır; koşul değişince görünüm gelir ya da gider. .transition(_:) değiştiricisi bu giriş ve çıkış için bir biçim seçer: solma, kayma, ölçek ya da bunların birleşimi. AnyTransition.asymmetric(insertion:removal:) ise girişe ve çıkışa ayrı biçimler verir. Bir geçişin görülmesi için eklenme ya da çıkarılma değişikliğinin bir animasyonla sarılması gerekir; animasyon yoksa görünüm geçişsiz olarak gelir ya da gider. Bu yüzden geçişler ikinci dersin devamıdır: animasyon değişikliği sarar, geçiş ise o değişikliğin ağaca etkisini biçimlendirir.
Ölçüm çekirdek dosyayla yapılır; çekirdek her derste aynı dosya adıyla ve aynı içerikle gelir.
// Invalidation.swift — bir degisikligin hangi gorunumu hangi kaynak yoluyla yeniden hesaplattigini siniflandirir
enum Storage {
case state(owner: String)
case model(object: String)
case environment
}
struct ViewNode {
let name: String
let parent: String?
let reads: Set<String>
init(_ name: String, parent: String?, reads: Set<String>) {
self.name = name
self.parent = parent
self.reads = reads
}
}
struct Recompute {
let view: String
let cause: String
}
struct ViewTree {
let nodes: [ViewNode]
let storage: [String: Storage]
func ancestors(of name: String) -> [String] {
var found: [String] = []
var current = nodes.first { $0.name == name }?.parent
while let next = current {
found.append(next)
current = nodes.first { $0.name == next }?.parent
}
return found
}
func cause(of node: ViewNode, key: String) -> String {
guard let kind = storage[key] else { return "olcum-disi" }
switch kind {
case .environment:
return "ortam"
case .model:
return "gozlem"
case .state(let owner):
if owner == node.name { return "yerel" }
if ancestors(of: node.name).contains(owner) { return "ebeveyn" }
return "kural-disi"
}
}
func change(_ label: String, key: String) -> [Recompute] {
let hits = nodes.filter { $0.reads.contains(key) }
.map { Recompute(view: $0.name, cause: cause(of: $0, key: key)) }
let skipped = nodes.filter { !$0.reads.contains(key) }.map(\.name)
let recomputed = hits.isEmpty ? "yok" : hits.map { "\($0.view) \($0.cause)" }.joined(separator: ", ")
let rest = skipped.isEmpty ? "yok" : skipped.joined(separator: ",")
print("\(label): \(key) degisti | yeniden \(recomputed) | atlanan \(rest)")
return hits
}
}
func countCauses(_ recomputes: [Recompute]) {
for cause in ["yerel", "ebeveyn", "gozlem", "ortam"] {
print("\(cause): \(recomputes.filter { $0.cause == cause }.count)")
}
}
Ayrıntı Alanı ve Liste
Aşağıdaki parça bir gelen kutusu kurar. İletiler gözlemlenebilir bir nesnede durur; liste bu nesneyi okur. Ekran, bir ayrıntı alanını isShowingDetail durumuna bağlı olarak gösterir ve düğme bu değişikliği animasyonla sarar.
import SwiftUI
@Observable
final class InboxStore {
var messages: [String] = []
var unreadCount = 0
}
struct InboxScreen: View {
let store: InboxStore
@State private var isShowingDetail = false
var body: some View {
VStack {
MessageList(store: store)
Button("Ayrıntı") {
withAnimation {
isShowingDetail.toggle()
}
}
if isShowingDetail {
DetailBanner()
.transition(.opacity)
}
}
}
}
struct MessageList: View {
let store: InboxStore
var body: some View {
List(store.messages, id: \.self) { message in
Text(message)
}
}
}
struct DetailBanner: View {
var body: some View {
Text("Ayrıntı")
}
}
Bu parça derlenmez ve çıktı vermez; yalnız kaynak olarak durur. Okuma düzeni şöyledir. MessageList, store.messages’ı okur; InboxStore bir gözlemlenebilir nesne olduğu için bu okuma gözlem kanalındandır. InboxScreen, isShowingDetail’ı gövdesindeki koşulda okur ve değerin sahibidir. DetailBanner hiçbir değer okumaz. Model, çerçevenin bağımlılık izleme kuralını kurar; çerçevenin kendisi değildir.
Şimdi iki değişiklik yazalım: modelden bir iletinin silinmesi ve ayrıntı alanının açılması. Ağaçtaki adlar kaynak parçadaki tip adlarıyla aynıdır, anahtarlar özellik adlarıdır.
// main.swift — silinen oge ve acilan ayrinti alani
let storage: [String: Storage] = [
"messages": .model(object: "InboxStore"),
"isShowingDetail": .state(owner: "InboxScreen"),
]
let screen = ViewTree(nodes: [
ViewNode("InboxScreen", parent: nil, reads: ["isShowingDetail"]),
ViewNode("MessageList", parent: "InboxScreen", reads: ["messages"]),
ViewNode("DetailBanner", parent: "InboxScreen", reads: []),
], storage: storage)
let recomputes = [
screen.change("ileti-silindi", key: "messages"),
screen.change("ayrinti-acildi", key: "isShowingDetail"),
].flatMap { $0 }
countCauses(recomputes)
ileti-silindi: messages degisti | yeniden MessageList gozlem | atlanan InboxScreen,DetailBanner ayrinti-acildi: isShowingDetail degisti | yeniden InboxScreen yerel | atlanan MessageList,DetailBanner yerel: 1 ebeveyn: 0 gozlem: 1 ortam: 0
Silinen ileti yalnız MessageList’i yeniden hesaplatır ve tetik gözlemdir: değişikliği ne listenin kendisi yaptı ne de ebeveyni; nesne değişti, liste o nesnenin ilgili özelliğini okuyordu. Ekran atlanır, çünkü messages’ı okumaz. Ayrıntı alanını açan değişiklik ise ekranı yerel tetikle yeniden hesaplatır; koşulu okuyan ve sahibi olan görünüm ekrandır. Listenin ve ayrıntı alanının satırda atlanan olarak görünmesi, bu iki görünümün o değişiklikte hesaplanmadığını söyler.
Dikkat edilecek bir nokta, ayrıntı alanının girişidir. Alan ağaca girdi, ama model bunu bir yeniden hesaplama olarak saymadı: DetailBanner atlananlar arasında. Bir görünümün ilk kez hesaplanması, var olan bir görünümün yeniden hesaplanması değildir ve model yalnız ikincisini ölçer.
EK9. Bu ölçüm yalnız messages’ı okuyan tek bir listenin bulunduğu ağaç için geçerlidir. Aynı nesneyi okuyan birden çok görünümün bulunduğu bir düzen ölçülmedi.
EK10. Bu ölçüm silme değişikliğini tek bir etiketle temsil eder. Listenin hangi satırının kalkacağı, satır kimliklerinin nasıl eşleştiği ve geçişin satıra uygulanıp uygulanmadığı ölçülmedi.
Gözlem İzleme Yalnız Okunan Özelliğe Tepki Verir
Gözlem kanalının kuralı “nesne değişti” değil, “okunan özellik değişti” biçimindedir. Bu kural modelin varsayımı değildir; gözlem izleme modülü sandbox’ta derlenir ve kural doğrudan ölçülür. Aşağıdaki blokta InboxStore iki özellik taşır; izleme yalnız messages’ı okur. Önce izlenmeyen unreadCount değişir, sonra izlenen messages.
// main.swift — gozlem izleme yalnizca okunan ozelligi gorur
import Observation
@Observable
final class InboxStore {
var messages: [String] = ["rapor", "fatura"]
var unreadCount = 0
}
let store = InboxStore()
withObservationTracking {
_ = store.messages
} onChange: {
print("izlenen ozellik degisti")
}
store.unreadCount += 1
print("unreadCount degisti")
store.messages.removeFirst()
print("messages degisti")
unreadCount degisti izlenen ozellik degisti messages degisti
İzlenmeyen unreadCount, geri çağrıyı tetiklemedi: ilk satırdan sonra geri çağrı iletisi gelmedi. İzlenen messages tetikledi. Geri çağrı atamanın içinde, eşzamanlı olarak koştu: iletisi, atamadan sonra gelen messages degisti satırından önce basıldı. Geri çağrının değer yazılmadan önce mi sonra mı koştuğu bu blokta ölçülmedi; ders bunu iddia etmez. Bu ölçüm, çerçevenin bu izlemeyi görünüm gövdesine nasıl bağladığını da göstermez. Çerçevenin gövdeyi bir izleme içinde hesapladığını söyleyebiliriz; bu bağın ayrıntısı kaynak okuma konusudur.
Bu iki ölçümün birlikte okunması gerekir. Modeldeki gozlem tetiği, izleme ölçümündeki davranışa dayanır: liste messages‘ı okuduğu için o özellik değişince yeniden hesaplanır, unreadCount değişince hesaplanmaz. Bir arayüzde okunmayan özelliğin değişmesi görünümü etkilemiyorsa, bunun nedeni görünümün o özelliği gövdesinde hiç kullanmamasıdır; nesneyi bir bütün olarak tutması yeterli değildir. Nesne düzeyinde bildirim yapan yaklaşım ise bu ayrımı yapmaz: ObservableObject ve @Published ile her yayımlanan değişiklik, nesneyi gözlemleyen tüm görünümleri geçersiz kılar. Bu yaklaşım Combine’a bağlıdır ve burada ölçülmez; kaynak olarak anılır.
EK11. Bu ölçüm izlemenin kendisini ölçer, görünüm ağacını değil. İzlemenin tek bir değişiklikten sonra bittiği ve ikinci değişiklik için yeniden kurulması gerektiği bu blokta gösterilmedi.
EK12. Bu ölçüm iki özellikli tek bir nesneyi kapsar. Nesne düzeyinde bildirim yapan yaklaşımın sayıları ölçülmedi.
Sınırlayıcı Ölçüm
Kalkan görünüm ne olur? Ayrıntı alanı kapandığında DetailBanner ağaçtan çıkar; bir yeniden hesaplama geçirmez, yeni bir değer okumaz, hesaplanmaz. Model bunu şöyle gösterir: ağaç, görünümü içerdiği kadar bilir. Aynı değişikliği, alanın ağaçta bulunduğu ve bulunmadığı iki ağaçta yazalım.
// main.swift — kalkan gorunum agacta yoktur
let storage: [String: Storage] = [
"isShowingDetail": .state(owner: "InboxScreen"),
]
let before = ViewTree(nodes: [
ViewNode("InboxScreen", parent: nil, reads: ["isShowingDetail"]),
ViewNode("MessageList", parent: "InboxScreen", reads: []),
ViewNode("DetailBanner", parent: "InboxScreen", reads: []),
], storage: storage)
let after = ViewTree(nodes: [
ViewNode("InboxScreen", parent: nil, reads: ["isShowingDetail"]),
ViewNode("MessageList", parent: "InboxScreen", reads: []),
], storage: storage)
let closing = before.change("ayrinti-kapaniyor", key: "isShowingDetail")
let closed = after.change("ayrinti-kapandi", key: "isShowingDetail")
print(closing.count == closed.count)
ayrinti-kapaniyor: isShowingDetail degisti | yeniden InboxScreen yerel | atlanan MessageList,DetailBanner ayrinti-kapandi: isShowingDetail degisti | yeniden InboxScreen yerel | atlanan MessageList true
Kapanan alan, ilk ağaçta atlanan listesinde görünür; ikinci ağaçta hiç görünmez. İki ağaçta yeniden hesaplanan görünüm aynıdır: yalnız InboxScreen. Kalkan görünüm hiçbir satırda yeniden hesaplananlar arasında yer almaz. Bu yüzden model, bir görünümün kaldırılmasını ayrı bir olay olarak saymaz. Çıkış geçişinin ne zaman bittiği, kalkan görünümün çıkış sırasında hesaplanıp hesaplanmadığı ve ağaçtan ne zaman tamamen silindiği ölçülmedi.
Özet
- Geçiş, görünümün ağaca girişini ve çıkışını biçimlendirir; görülmesi için değişikliğin animasyonla sarılması gerekir.
- Modelden silinen öğe, o modeli okuyan listeyi gözlem tetiğiyle yeniden hesaplatır.
- Gözlem izleme yalnız okunan özelliğe tepki verir; okunmayan özelliğin değişmesi geri çağrıyı tetiklemez.
- Ayrıntı alanını açan durum, sahibi olan ekranı yerel tetikle yeniden hesaplatır.
- Kalkan görünüm yeniden hesaplanmaz, ağaçtan çıkar; model bu çıkışı sayılan bir olay olarak ele almaz.
- Bu dersin tetiği: gözlem.
Sonraki Adım
Şimdiye kadar değerlerin kaynağı ya bir görünümün durumu ya da bir model nesnesiydi. Sonraki derste kaynağı kullanıcının sistem ayarları olan değerlere bakacağız: dinamik yazı boyutu ve hareket azaltma tercihi gibi ortam değerleri, erişilebilirlik özellikleriyle birlikte hangi görünümleri yeniden hesaplatır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.