Ders 09 / 21
Yerel Durum
Bir görünümün kendi durumunun yalnız o görünümü yeniden hesaplattığının, aynı durum ataya kaldırılınca tetiğin ebeveyne döndüğünün ve kardeşin durumunun kural dışı kaldığının ölçümü.
Yayın Güncelleme
Bu derste
- Görünümün kendi durumunu, değerin sahibinin o görünüm olduğu yerel tetikle ilişkilendirir
- Aynı durumun ataya kaldırılmasının tetiği yerelden ebeveyne çevirdiğini ölçer
- Durumun yalnız sahibinden altındaki görünümlere aktığını, kardeşe akmadığını gösterir
- Durum kaldırmanın hangi görünümleri yeniden hesaplattığını okuma yollarından çıkarır
İçindekiler
Önceki konu görünümlerin nasıl kurulduğunu tamamladı: bir görünüm, değiştiricilerle ve kurucularla tarif edilen bir değerdi. Bu konu o tarifin içindeki değişkenlere bakar. Açılış dersinde altı değişikliğin sekiz yeniden hesaplama ürettiğini ve bunların ikisinin yerel olduğunu gördük; yerel iki satırın ikisi de bir görünümün kendi değerinden geliyordu. Bu derste bakılan tetik yerel: yeniden hesaplamayı, değerin sahibinin kendisi olan görünüm yaşıyor.
Ölçüm yine aşağıdaki çekirdek dosyayla yapılır. Ders boyunca değişmez, kurs boyunca her ders onu birebir verir.
// 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)")
}
}
Storage.state(owner:) bir değerin hangi görünüme ait olduğunu söyler. cause işlevi bu sahibi, değişiklikten etkilenen görünümle karşılaştırır: sahip görünümün kendisiyse tetik yerel, sahip görünümün bir atasıysa ebeveyn, ikisi de değilse kural-disi olur. Model, çerçevenin bağımlılık izleme kuralını kurar; çerçevenin kendisi değildir.
Görünümün Kendi Durumu
Bir görünümün zamanla değişen ve yalnız kendisini ilgilendiren değeri yerel durum (local state) olarak tutulur. Bu değer bir özellik sarmalayıcısıyla, @State ile işaretlenir. Aşağıdaki görünüm bir dokunma sayacı tutar. Sayaç private işaretlidir: görünümün dışına açılmaz, yalnız görünümün kendi gövdesi onu okur ve yazar.
import SwiftUI
struct CounterCard: View {
@State private var tapCount = 0
var body: some View {
VStack {
Text("Dokunma: \(tapCount)")
Button("Artir") { tapCount += 1 }
}
}
}
@State ile işaretlenen değer görünüm değerinin içinde değil, çerçevenin yönettiği bir depoda yaşar. Görünüm bir yapıdır ve her yeniden hesaplamada yeniden kurulabilir; sayaç ise kurulan her yeni değerde sıfırlanmaz, çünkü değeri saklayan görünüm değil depodur. Sahip, bu depoyu kendi kimliği üzerinden kullanan görünümdür. Bu ayrım ölçümde owner alanına dönüşür.
Aşağıdaki ağaçta DashboardView bir kök, CounterCard onun çocuğu ve StatusLabel onun ikinci çocuğudur. Yukarıdaki kaynak parçadaki tip ve özellik adları ağaçta aynen durur: anahtar tapCount, sahip CounterCard. Görünümlerin hangi değeri okuduğu, kaynaktaki gövdelere göre reads içindedir; DashboardView ve StatusLabel sayacı okumaz.
// main.swift — gorunumun kendi durumu yalniz kendini yeniden hesaplatir
let storage: [String: Storage] = [
"tapCount": .state(owner: "CounterCard"),
]
let screen = ViewTree(nodes: [
ViewNode("DashboardView", parent: nil, reads: []),
ViewNode("CounterCard", parent: "DashboardView", reads: ["tapCount"]),
ViewNode("StatusLabel", parent: "DashboardView", reads: []),
], storage: storage)
let recomputes = [
screen.change("dokunuldu", key: "tapCount"),
].flatMap { $0 }
countCauses(recomputes)
dokunuldu: tapCount degisti | yeniden CounterCard yerel | atlanan DashboardView,StatusLabel yerel: 1 ebeveyn: 0 gozlem: 0 ortam: 0
Tek değişiklik, tek yeniden hesaplama: yalnız CounterCard yeniden hesaplandı ve tetik yerel. DashboardView ve StatusLabel atlandı, çünkü hiçbiri tapCount değerini okumuyor. Bu, açılış dersindeki satir-acildi satırının aynısıdır: değer görünümün kendisinindir ve değişikliğin etkisi o görünümde kalır. Çıktının son dört satırında dört kategorinin sayımı görünür; yerel bir, geri kalan üçü sıfır.
Yerel durumun getirisi budur: değişikliğin kapsamı sahibin gövdesiyle sınırlıdır. Ekranın başka bir yerinde yaşayan görünüm sayacı bilmez ve bilmek zorunda da kalmaz.
DR1. Bu ölçüm tek bir değişiklik ve tek bir anahtar için. Sayaç değerinin görünümün yeniden kurulduğu her seferde korunduğu, modelde bir cümleyle anlatıldı ama ölçülmedi; modelin ViewTree tipi değerin ömrünü taşımaz.
Durumu Ataya Kaldırmak
Başka bir görünüm de sayacı bilmek isterse yerel durum yetmez. Durumu, ilgilenen görünümlerin ortak atasına taşımak gerekir; buna durum kaldırma (state lifting) denir. Ata durumun sahibi olur, değeri çocuğa parametre olarak verir ve değeri değiştirmek için çocuğa bir kapanış iletir.
import SwiftUI
struct DashboardView: View {
@State private var tapCount = 0
var body: some View {
VStack {
CounterCard(tapCount: tapCount, onTap: { tapCount += 1 })
StatusLabel()
}
}
}
struct CounterCard: View {
let tapCount: Int
let onTap: () -> Void
var body: some View {
VStack {
Text("Dokunma: \(tapCount)")
Button("Artir", action: onTap)
}
}
}
Görünüm yine aynı ekranı çiziyor; değişen, değerin yaşadığı yerdir. CounterCard artık tapCount saklamaz, yalnız ona verilen değeri gösterir. Sahip DashboardView olmuştur ve gövdesi değeri çocuğa ilettiği için onu okur. Sahiplik taşındığı için ağacın saklama tablosu da değişir: aynı anahtar, yeni bir sahip.
// main.swift — durum ataya kaldirilinca ayni anahtar ebeveyn tetigi uretir
let storage: [String: Storage] = [
"tapCount": .state(owner: "DashboardView"),
]
let screen = ViewTree(nodes: [
ViewNode("DashboardView", parent: nil, reads: ["tapCount"]),
ViewNode("CounterCard", parent: "DashboardView", reads: ["tapCount"]),
ViewNode("StatusLabel", parent: "DashboardView", reads: []),
], storage: storage)
let recomputes = [
screen.change("dokunuldu", key: "tapCount"),
].flatMap { $0 }
countCauses(recomputes)
dokunuldu: tapCount degisti | yeniden DashboardView yerel, CounterCard ebeveyn | atlanan StatusLabel yerel: 1 ebeveyn: 1 gozlem: 0 ortam: 0
Aynı değişiklik şimdi iki görünümü yeniden hesaplattı. DashboardView değerin sahibi olduğu için yerel; CounterCard değeri okuyan ama sahibi olmayan bir alt görünüm olduğu için ebeveyn. StatusLabel hâlâ atlanıyor: ata bir değeri sahiplendi diye her çocuğu yeniden hesaplatmaz, yalnız değeri okuyanları. Durum kaldırmanın bedeli bu: aynı dokunuş bir yerine iki yeniden hesaplama üretir. Karşılığında sayacı ekranın başka görünümleri de okuyabilir hâle gelir.
Sayılar önceki ölçümle kıyaslanınca tablo şöyle okunur: yerel durum bir yeniden hesaplama, yerel bir; ataya kaldırılmış durum iki yeniden hesaplama, yerel bir ve ebeveyn bir. Değişen şey değerin kendisi değil sahibidir. Aynı CounterCard ilk ölçümde yerel, ikincisinde ebeveyn tetikle yeniden hesaplandı; tetik görünümün türüne değil, değerin sahibine bağlıdır.
DR2. Bu ölçüm iki sahiplik düzeni için: sayaç ya CounterCard ya DashboardView tarafından sahiplenildi. Sahipliğin kök görünümden daha derindeki bir ataya kaldırılması ve birden çok çocuğun aynı değeri okuması ölçülmedi.
Yerel Kalmalı mı, Kaldırılmalı mı
İki ölçüm bir karar kuralı verir ve kural değerin kimler tarafından okunduğuna bakar. Değeri yalnız bir görünüm okuyorsa durum o görünümde kalır: değişiklik tek yeniden hesaplama üretir ve başka görünümün gövdesine dokunmaz. Değeri birden çok görünüm okuyorsa durum bu görünümlerin en yakın ortak atasına çıkar; çünkü değerin tek bir doğruluk kaynağı olmalıdır. İki görünüm ayrı ayrı sayaç tutarsa iki sayaç birbirinden bağımsız değişir ve ekran aynı şeyi iki farklı biçimde gösterir.
İkinci ölçümün bir yan bulgusu da vardır: ata durumu sahiplenince çocuklar yeniden hesaplanır, ama bu yalnız değeri okuyan çocuklar için geçerlidir. Okumayan çocuk, ikinci ölçümdeki StatusLabel gibi, atlanır. Durumu mümkün olan en alçak ortak atada tutmak bu yüzden iki isteği birden karşılar: değeri okuyanlara ulaştırır, okumayanları etkilemez.
Sınırlayıcı Ölçüm
Durumun bir sahibi vardır ve değer yalnız sahibinden altındaki görünümlere akar. İki kardeş görünüm birbirinin durumuna ulaşamaz. Aşağıdaki ağaçta sayaç CounterCard’a aittir, ama kardeşi ResetButton da onu okuyor: bir ata–torun yolu yok, ikisi yalnız ortak bir atayı paylaşıyor.
// main.swift — kardesin durumunu okuyan gorunum kural disi kalir
let storage: [String: Storage] = [
"tapCount": .state(owner: "CounterCard"),
]
let screen = ViewTree(nodes: [
ViewNode("DashboardView", parent: nil, reads: []),
ViewNode("CounterCard", parent: "DashboardView", reads: ["tapCount"]),
ViewNode("ResetButton", parent: "DashboardView", reads: ["tapCount"]),
], storage: storage)
let recomputes = [
screen.change("dokunuldu", key: "tapCount"),
].flatMap { $0 }
countCauses(recomputes)
dokunuldu: tapCount degisti | yeniden CounterCard yerel, ResetButton kural-disi | atlanan DashboardView yerel: 1 ebeveyn: 0 gozlem: 0 ortam: 0
CounterCard yerel olarak yeniden hesaplandı. ResetButton ise sahibin ne atası ne kendisi olduğu için kural-disi satırı aldı: model bu okumayı dört kategoriden birine koyamaz. Bu bir kategori değildir ve countCauses bu satırı saymaz; altta yalnız CounterCard’ın yerel yeniden hesaplaması görünür. Kuralın söylediği şudur: durum, sahibine ve sahibinin altındaki görünümlere aittir. Bir kardeşin okuması gerekiyorsa durum ortak ataya kaldırılır; bu dersin ikinci bölümündeki düzen bunun yoludur.
DR3. Bu ölçüm yalnız bir kardeş okuması için ve modelin kurduğu kural içindir. Çerçevenin kardeşin durumuna ulaşmaya çalışan kodu nasıl işlediği ölçülmedi.
Özet
- Görünümün kendi durumu yerel durumdur; değerin sahibi o görünümdür.
- Yerel durum değişince yalnız sahip görünüm yeniden hesaplanır, tetik yereldir.
- Durumu ataya kaldırmak sahibi değiştirir: ata yerel, değeri okuyan çocuk ebeveyn olur.
- Aynı görünüm, değerin sahibine bağlı olarak bir ölçümde yerel, ötekinde ebeveyn tetikle yeniden hesaplanır.
- Durum yalnız sahibinden altındaki görünümlere akar; kardeşin okuması
kural-disikalır ve sayılmaz. - Bu dersin tetiği: yerel.
Sonraki Adım
Durumu ataya kaldırdığımızda çocuğa bir değer ve bir kapanış verdik; çocuk değeri yalnız bu kapanış üzerinden değiştirebiliyordu. Sıradaki derste aynı iş tek bir kavramla yapılır: çocuğa değerin kendisi değil, değerin sahibine açılan bir bağ verilir. Soru aynı kalır, ama değişikliği çocuk başlatırken yeniden hesaplamayı kimin tetiklediği değişir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.