Ders 01 / 21
Bildirimsel Sözdizim
Bir görünüm ağacında altı değişikliğin dört farklı tetikle sekiz yeniden hesaplama ürettiğinin ve izlenmeyen değerin hiçbir şeyi yeniden hesaplatmadığının ölçümü.
Yayın Güncelleme
Bu derste
- Bildirimsel arayüzde görünümün durumdan türetildiğini buyurgan yolla karşılaştırarak ayırır
- Bir değişikliğin hangi görünümleri yeniden hesaplattığını okunan değerler üzerinden ölçer
- Yeniden hesaplamanın dört tetiğini (yerel, ebeveyn, gözlem, ortam) saklama yerinden çıkarır
- Değeri okumayan görünümün yeniden hesaplanmadığını, atlandığını gösterir
İçindekiler
Önceki kurs platformun katmanlarını, çerçevelerini ve sürümlerini tanıttı; bu kurs tanımdan değişikliğe geçer. Bildirimsel bir arayüzde ekranı tarif eden kod, ekranın nasıl güncelleneceğini değil, belli bir durumda neye benzeyeceğini söyler. Durum değişince çerçeve tarifi yeniden okur, ama tarifi yazan her görünümü değil, yalnız değişen değeri okuyanları. Bu kursun her dersi aynı soruyu sorar: bu yeniden hesaplamayı ne tetikledi? Bu derste bakılan tetik, dördünün de dağılımı: altı değişiklik, dört tetik.
Ölçümün kalbi aşağıdaki çekirdek dosyadır. Ders boyunca değişmez; kurs boyunca her ders onu birebir verir ve kendi görünüm ağacını üstüne kurar.
// 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)")
}
}
Çekirdek iki şeyi ayrı tutar. Storage, bir değerin nerede saklandığını söyler: bir görünümün kendi durumu (state), gözlemlenebilir bir nesnenin özelliği (model) ya da ortam değeri (environment). ViewNode ise bir görünümün neyi okuduğunu söyler. change bir anahtar verildiğinde o anahtarı okuyan görünümleri bulur ve her biri için cause işlevini çalıştırır. Tetiği etiket vermez; kural verir: ortam değeri ortam, nesne özelliği gözlem, görünümün kendi durumu yerel, bir atasının durumu ebeveyn sayılır. Model, çerçevenin bağımlılık izleme kuralını kurar; çerçevenin kendisi değildir.
Altı Değişiklik
Aşağıdaki ağaç bir görev ekranını kurar: ekranın kendisi, bir başlık, bir süzgeç çubuğu, bir liste, listenin satırı ve bir ilerleme rozeti. Her görünümün okuduğu değerler reads içindedir, her değerin saklandığı yer storage içindedir. Altı değişiklik verilir ve her biri için hangi görünümlerin yeniden hesaplandığı basılır.
// main.swift — alti degisiklik, dort tetik
let storage: [String: Storage] = [
"filter": .state(owner: "TaskScreen"),
"isExpanded": .state(owner: "TaskRow"),
"tasks": .model(object: "TaskStore"),
"completedCount": .model(object: "TaskStore"),
"dynamicTypeSize": .environment,
"colorScheme": .environment,
]
let screen = ViewTree(nodes: [
ViewNode("TaskScreen", parent: nil, reads: ["filter"]),
ViewNode("HeaderView", parent: "TaskScreen", reads: ["dynamicTypeSize"]),
ViewNode("FilterBar", parent: "TaskScreen", reads: ["filter"]),
ViewNode("TaskList", parent: "TaskScreen", reads: ["filter", "tasks"]),
ViewNode("TaskRow", parent: "TaskList", reads: ["isExpanded", "colorScheme"]),
ViewNode("ProgressBadge", parent: "TaskScreen", reads: ["completedCount"]),
], storage: storage)
let recomputes = [
screen.change("filtre-secildi", key: "filter"),
screen.change("satir-acildi", key: "isExpanded"),
screen.change("gorev-eklendi", key: "tasks"),
screen.change("gorev-bitti", key: "completedCount"),
screen.change("yazi-buyudu", key: "dynamicTypeSize"),
screen.change("karanlik-kip", key: "colorScheme"),
].flatMap { $0 }
countCauses(recomputes)
filtre-secildi: filter degisti | yeniden TaskScreen yerel, FilterBar ebeveyn, TaskList ebeveyn | atlanan HeaderView,TaskRow,ProgressBadge satir-acildi: isExpanded degisti | yeniden TaskRow yerel | atlanan TaskScreen,HeaderView,FilterBar,TaskList,ProgressBadge gorev-eklendi: tasks degisti | yeniden TaskList gozlem | atlanan TaskScreen,HeaderView,FilterBar,TaskRow,ProgressBadge gorev-bitti: completedCount degisti | yeniden ProgressBadge gozlem | atlanan TaskScreen,HeaderView,FilterBar,TaskList,TaskRow yazi-buyudu: dynamicTypeSize degisti | yeniden HeaderView ortam | atlanan TaskScreen,FilterBar,TaskList,TaskRow,ProgressBadge karanlik-kip: colorScheme degisti | yeniden TaskRow ortam | atlanan TaskScreen,HeaderView,FilterBar,TaskList,ProgressBadge yerel: 2 ebeveyn: 2 gozlem: 2 ortam: 2
Çıktıyı satır satır okuyalım. filtre-secildi değişikliğinde TaskScreen değeri kendi durumu olarak saklar, yerel; FilterBar ve TaskList aynı değeri okur ama sahibi onlar değildir, ebeveyn. HeaderView ve ProgressBadge süzgeci okumaz, atlanır. satir-acildi değişikliğinde yalnız TaskRow yeniden hesaplanır, çünkü isExpanded onun kendi durumudur; ekranın geri kalanı bu değişiklikten haberdar bile olmaz. gorev-eklendi ve gorev-bitti nesnenin özelliklerini değiştirir ve gözlem sayılır. yazi-buyudu ve karanlik-kip ortam değerleridir ve ortam sayılır.
Sayılar şöyle çıkar: altı değişiklik sekiz yeniden hesaplama üretti; ikisi yerel, ikisi ebeveyn, ikisi gözlem, ikisi ortam. Değişiklik sayısı ile yeniden hesaplama sayısı aynı şey değildir: filtre-secildi tek bir değişiklikle üç görünümü yeniden hesaplattı. Dağılımın eşit çıkması kurala değil, ağacı kurarken yaptığımız seçime aittir; ders bunu bir bulgu olarak sunmaz.
GV1. Bu ölçüm yalnız altı değişiklik ve altı anahtar için. Aynı anda iki değerin değişmesi ve bir görünümün hem bir durumu hem bir ortam değerini okuduğu değişiklik sırası ölçülmedi.
Süzgecin İki Okuyucusu
İlk satırdaki ebeveyn etiketi, kategorinin neye baktığını gösterir. FilterBar ve TaskList, filter değerinin sahibi değildir; değerin sahibi TaskScreen’dir ve sahip, ancestors ile bulunan atalar arasındadır. Kategori, değişikliği kimin başlattığına değil, değerin kimde saklandığına ve onu kimin okuduğuna bakar. Süzgeç çubuğu değeri bir bağlama (binding) ile alır: ekranın durumuna uzanan bir referanstır, kendi kopyası değildir. Liste ise aynı değeri parametre olarak alır.
// Kaynak parca: FilterBar suzgeci ebeveynin durumundan baglama ile alir
import SwiftUI
struct FilterBar: View {
@Binding var filter: String
var body: some View {
Picker("Suzgec", selection: $filter) {
Text("Hepsi").tag("hepsi")
Text("Acik").tag("acik")
}
.pickerStyle(.segmented)
}
}
struct TaskScreen: View {
@State private var filter = "hepsi"
var body: some View {
VStack {
FilterBar(filter: $filter)
Text("Suzgec: \(filter)")
}
}
}
TaskScreen değeri @State ile saklar, $filter ile bağlamayı üretir. Her iki görünüm de gövdesinde filter değerini kullanır; bu yüzden ağaçta ikisinin de reads kümesinde bu anahtar vardır. Bir görünümün yeniden hesaplanması, değeri okuyup okumadığına bağlıdır, ağaçtaki yerine değil: HeaderView aynı ekranın çocuğudur ama süzgeci okumaz ve atlanır. Çocuk olmak bir bağımlılık değildir, okumak bağımlılıktır.
Bu parça da KAYNAK’tır. Model, çerçevenin okuma kuralını kurar; bağlamanın nasıl çalıştığı, yani değer yazıldığında sahibin durumunun nasıl güncellendiği bu derste ölçülmedi.
Buyurgan Yol ve Bildirimsel Yol
Buyurgan bir arayüz katmanında her değişikliği kod elle yayar: sayaç artınca etiketi bulur, metnini yazarsınız. Aşağıdaki parça bunu gösterir. Etiketin güncel kalması, her değişiklik yolunda label.text satırının yazılmasına bağlıdır; bir yolu unutursanız ekran eskir.
// Kaynak parca: buyurgan yol, etiket her degisiklikte elle guncellenir
import UIKit
final class CounterController: UIViewController {
private let label = UILabel()
private var count = 0
func increment() {
count += 1
label.text = "Sayi: \(count)"
}
}
Bildirimsel yolda aynı ekran bir tariftir. Düğmenin metni count değerinden türer; count bir özellik sarmalayıcısıyla (property wrapper) durum olarak işaretlenmiştir, yani çerçeve onu izler. Kod label.text yazmaz; yalnız count değerini artırır ve gövdenin (body) yeniden hesaplanmasını çerçeve üstlenir.
// Kaynak parca: bildirimsel yol, gorunum durumdan turer
import SwiftUI
struct CounterView: View {
@State private var count = 0
var body: some View {
Button("Sayi: \(count)") {
count += 1
}
}
}
İki parçanın farkı kod uzunluğunda değil, sorumluluğun yerindedir. Buyurgan yolda “ekran şimdi neye benzemeli” sorusunun cevabı, değişikliği yapan her satıra dağılır. Bildirimsel yolda cevap tek yerde, gövdededir ve gövde yalnız okuduğu değerlere bağlıdır. Bu yüzden ölçü hangi görünümün hangi değeri okuduğudur: bağımlılık, tarifin içindeki okumadan doğar. Her iki parça da KAYNAK’tır; platform çerçevesine dayanır, çıktısı iddia edilmez.
GV2. Bu bölüm yalnız iki sayaç parçasını karşılaştırır. Buyurgan yolun hangi durumlarda bildirimsel yoldan daha az kod yazdırdığı ve iki yolun çalışma zamanı maliyeti ölçülmedi.
Gövde Durumun İşlevidir
Ölçülen ağaçtaki TaskRow görünümünü gerçek kodla yazmak, reads kümesinin nereden geldiğini gösterir. Aşağıdaki parçada gövde iki şeyi okur: görünümün kendi durumu isExpanded ve ortamdan gelen colorScheme. İki okuma, ağaçtaki reads: ["isExpanded", "colorScheme"] satırının karşılığıdır.
// Kaynak parca: TaskRow govdesi kendi durumunu ve ortam degerini okur
import SwiftUI
struct TaskRow: View {
let title: String
@State private var isExpanded = false
@Environment(\.colorScheme) private var colorScheme
var body: some View {
VStack(alignment: .leading) {
Text(title)
if isExpanded {
Text("Ayrinti")
}
}
.foregroundStyle(colorScheme == .dark ? Color.white : Color.black)
.onTapGesture { isExpanded.toggle() }
}
}
title bir sabittir ve ağaçta anahtar değildir: görünüm onu kendisi değiştiremez, ebeveyn yeni bir değerle TaskRow kurarsa görünümün gövdesi yeniden hesaplanır. Ölçülen modelde ebeveynin kurduğu değerler, ebeveynin durumuna bağlı olduklarında ebeveyn tetiğine dönüşür; TaskList’in filter okuması böyledir. Model, çerçevenin bu kuralını kurar: görünüm okuduğu değere bağlıdır, okumadığına bağlı değildir.
Bu yüzden satir-acildi satırında beş görünüm atlandı. Bir satırın açılması ekranın başlığını, süzgeç çubuğunu, listeyi ve rozeti ilgilendirmez; hiçbiri isExpanded değerini okumaz. Bildirimsel arayüzün getirisi budur: ekranın tümünü her değişiklikte baştan kurmak yerine, değişen değerin okunduğu yerleri yeniden hesaplamak. Bağımlılık, tarifin içindeki okumadan doğar.
Sınırlayıcı Ölçüm
Her değer bir tetik üretmez. Çerçevenin izlediği bir yerde saklanmayan sıradan bir değişken değişince hiçbir görünüm yeniden hesaplanmaz; gövde eskisi gibi kalır. Aşağıdaki ağaçta üç değişiklik var. filter saklama tablosundadır. scrollOffset bir görünüm tarafından okunur ama tabloda yoktur: kaynağı bilinmiyor. visitCount ise tabloda yoktur ve hiçbir görünüm tarafından okunmaz.
// main.swift — saklama tablosunda olmayan anahtar tetik uretmez
let storage: [String: Storage] = [
"filter": .state(owner: "TaskScreen"),
]
let screen = ViewTree(nodes: [
ViewNode("TaskScreen", parent: nil, reads: ["filter", "scrollOffset"]),
ViewNode("FilterBar", parent: "TaskScreen", reads: ["filter"]),
], storage: storage)
let recomputes = [
screen.change("filtre-secildi", key: "filter"),
screen.change("kaydirma-degisti", key: "scrollOffset"),
screen.change("sayac-artti", key: "visitCount"),
].flatMap { $0 }
countCauses(recomputes)
filtre-secildi: filter degisti | yeniden TaskScreen yerel, FilterBar ebeveyn | atlanan yok kaydirma-degisti: scrollOffset degisti | yeniden TaskScreen olcum-disi | atlanan FilterBar sayac-artti: visitCount degisti | yeniden yok | atlanan TaskScreen,FilterBar yerel: 1 ebeveyn: 1 gozlem: 0 ortam: 0
kaydirma-degisti satırı çekirdeğin ölçemediği satırdır: TaskScreen değeri okuduğu için satırda listelenir, ama değer tabloda olmadığı için kural onu dört kategoriden birine koyamadı ve satır olcum-disi yazar. Bu satır bir yeniden hesaplama iddiası değildir; çerçevede saklanmayan bir değerin değişmesi yeniden hesaplama doğurmaz. olcum-disi bir kategori değildir; “bu değerin nereden geldiği modelde yok” demektir ve countCauses bu satırı saymaz, altta yalnız filtre-secildi satırının yeniden hesaplamaları görünür: bir yerel, bir ebeveyn. sayac-artti satırında hiçbir görünüm değeri okumuyor; yeniden yok ve iki görünüm de atlanır. Çerçevede bu, izlenmeyen sıradan bir değişkenin değişmesine denk gelir: değişkenin değeri değişir, ekran değişmez.
Ders için sonuç bir uyarıdır: bir değerin ekranı güncellemesi, ona yazılmış olmasıyla değil, çerçevenin izlediği bir yerde saklanmasıyla olur. Hangi saklama yerlerinin izlendiği, kursun geri kalanının konusudur.
GV3. Bu ölçüm yalnız tabloda olmayan iki anahtar için. Çerçevenin izlenmeyen bir değişkeni gövdede okuyan görünümü ilk çizimde nasıl gösterdiği ve değişiklikten sonra eski değeri ne kadar süre koruduğu ölçülmedi.
Özet
- Bildirimsel arayüzde görünüm durumdan türer; değişikliği yayan kod yoktur, okuyan görünüm vardır.
- Değişen değeri okuyan görünüm yeniden hesaplanır, okumayan atlanır.
- Tetik, değerin saklandığı yere bakılarak çıkarılır: kendi durumu yerel, atanın durumu ebeveyn, nesne özelliği gözlem, ortam değeri ortam.
- Altı değişiklik sekiz yeniden hesaplama üretti: yerel 2, ebeveyn 2, gözlem 2, ortam 2.
- Saklama tablosunda olmayan anahtar çekirdeğin ölçemediği bir
olcum-disisatırı üretir; bu satır yeniden hesaplama iddiası değildir ve sayılmaz; kimsenin okumadığı anahtar hiçbir şeyi yeniden hesaplatmaz. - Bu dersin tetiği: dördünün de dağılımı (2/2/2/2).
Sonraki Adım
Bu derste ağaç ve okumalar hazırdı; görünümün kendisini nasıl kurduğumuza bakmadık. Sıradaki derste bir görünümün üzerine değiştirici eklemek, o görünümün ve altındakilerin neyi okuduğunu değiştirir: kapsayıcıya konan bir yazı tipi alt ağaca yayılır ve ortam olarak okunur. Soru aynı kalır, ama bu kez tetik, değiştiricinin yayılımından gelir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.