---
title: 'SwiftUI ile Arayüz'
source: 'https://academia.sh/tr/kurslar/swiftui'
language: tr
updated: '2026-10-21T05:03:13+00:00'
license: 'CC BY-SA 4.0'
lessons: 21
outcomes:
    - 'Bildirimsel görünüm ağacını durum kaynaklarıyla kurar'
    - 'Düzen sistemini ve öncelik kurallarını doğru uygular'
    - 'Erişilebilir ve yerelleştirilmiş arayüz üretir'
    - 'Bir yeniden hesaplamanın görünümün kendi durumundan mı, atasından mı, gözlenen bir modelden mi yoksa ortamdan mı tetiklendiğini ayırt eder'
    - 'Bağımlılık izleme kuralını küçük bir modelle kurar ve modelin çerçevenin kendisi olmadığını tanır'
---

# SwiftUI ile Arayüz

Bildirimsel bir arayüzde görünüm durumdan türetilir; durum değişince bazı görünümler yeniden hesaplanır, bazıları atlanır. Kurs her düzeneği tek bir soruyla ölçer — bu yeniden hesaplamayı ne tetikledi? Görünümün kendi durumu mu, atasından gelen bir değer mi, gözlenen bir model mi, yoksa ağaç boyunca yayılan bir ortam değeri mi? Değiştiriciler, yığıt ve ızgara düzenleri, listeler, bağlama, gözlemlenebilir nesneler, gezinme ve sunum, hareketler, animasyonlar, erişilebilirlik ve yerelleştirme aynı çerçeveden geçirilir.

## Görünüm Ağacı Modeli

Görünümün durumdan türetilmesi, değiştiriciler ve sıralarının etkisi, dikey, yatay ve derinlik yığıtları, önerilen boyut ve öncelik görüşmesi, ızgara ve geometri okuma, veri güdümlü liste ve form, metin, görsel ve şekil bileşenleri, yeniden kullanılabilir görünüm kurucular.

### Bildirimsel Sözdizim

Source: https://academia.sh/tr/kurslar/swiftui/bildirimsel-sozdizim

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ü.

Ö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.

```swift
// 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.

```swift
// 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.

```swift
// 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.

```swift
// 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.

```swift
// 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.

```swift
// 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.

```swift
// 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-disi` satı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.

### Görünümler ve Değiştiriciler

Source: https://academia.sh/tr/kurslar/swiftui/gorunumler-ve-degistiriciler

Kapsayıcıya konan yazı tipi ve renk değiştiricilerinin alt ağaca ortam değeri olarak yayıldığının, değiştirici sırasının sonucu değiştirdiğinin ve kendi yazı tipini koyan görünümün ortamı okumadığının ölçümü.

Önceki derste altı değişiklik dört tetik üretti ve bir ağacın okuma kümelerinden yeniden hesaplamaların çıkarıldığını gördük. Bu derste ağaca ilk kez dışarıdan bakmayı bırakıp görünümü kuran araca bakıyoruz: değiştirici. Bir görünüme `.font`, `.padding` ya da `.foregroundStyle` eklemek, o görünümü değiştirmez; eklenen her değiştirici, görünümü saran yeni bir görünüm değeri döndürür. Bazı değiştiriciler yalnız o görünümün görünüşünü etkiler, bazıları ise altındaki bütün ağaca yayılır. Soru aynı kalır: **bu yeniden hesaplamayı ne tetikledi?** Bu derste bakılan tetik, **ortam**.

```swift
// 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, açılış dersindekiyle aynıdır. Bu dersin ağacı bir profil kartıdır: kartı kuran ekran, kartın kendisi, bir başlık, bir gövde metni ve bir alt bilgi.

#### Alt Ağaca Yayılan Değiştirici

Yazı tipi ve ön plan rengi, bir görünümün değil, bir alt ağacın özelliğidir. Bir kapsayıcıya `.font(.title2)` konduğunda içindeki bütün metinler o yazı tipini kullanır; metnin kendisi bu değeri bir parametre olarak almamıştır, ortamdan okur. Bu, ortam değeri (environment value) mekanizmasıdır: değer ağaçta yukarıdan aşağıya akar ve okuyan her görünüm onu gövdesinde kullanır. Aşağıdaki parça bunu kaynak olarak gösterir.

```swift
// Kaynak parca: kapsayiciya konan font ve renk alt agaca yayilir
import SwiftUI

struct ProfileCard: View {
    let headline: String
    let isDimmed: Bool

    var body: some View {
        VStack(alignment: .leading) {
            Text(headline)
            Text("Yazilim muhendisi")
            Text("Son gorulme: bugun")
        }
        .font(.title2)
        .foregroundStyle(.secondary)
        .opacity(isDimmed ? 0.5 : 1)
    }
}
```

Üç metnin hiçbirine yazı tipi verilmedi; üçü de kapsayıcının `.font` değerini okur. `.opacity` ise ortam değeri olarak alt ağaca yayılmaz, kapsayıcının çiziminde uygulanır ve bu parçada kartın parametresinden gelir. Model bu iki yolu ayırır: ortamdan okunan değer ortam, ebeveynin verdiği değer ebeveyn tetiği üretir. Ağaçta `font` ve `foregroundStyle` ortam değeri, `headline` ve `isDimmed` ise kartı kuran ekranın, `ProfileScreen`'in durumu olarak saklanır; kart bu iki değeri `let` parametresi olarak alır, sahibi değildir.

```swift
// main.swift — kapsayiciya konan degisiklik alt agaca ortam olarak yayilir
let storage: [String: Storage] = [
    "font": .environment,
    "foregroundStyle": .environment,
    "headline": .state(owner: "ProfileScreen"),
    "isDimmed": .state(owner: "ProfileScreen"),
]

let screen = ViewTree(nodes: [
    ViewNode("ProfileScreen", parent: nil, reads: ["headline", "isDimmed"]),
    ViewNode("ProfileCard", parent: "ProfileScreen", reads: ["headline", "isDimmed"]),
    ViewNode("CardTitle", parent: "ProfileCard", reads: ["headline", "font"]),
    ViewNode("CardBody", parent: "ProfileCard", reads: ["font", "foregroundStyle"]),
    ViewNode("CardFooter", parent: "ProfileCard", reads: ["font"]),
], storage: storage)

let recomputes = [
    screen.change("yazi-tipi-degisti", key: "font"),
    screen.change("baslik-degisti", key: "headline"),
    screen.change("yazi-rengi-degisti", key: "foregroundStyle"),
    screen.change("soluk-yapildi", key: "isDimmed"),
].flatMap { $0 }

countCauses(recomputes)
```

```
yazi-tipi-degisti: font degisti | yeniden CardTitle ortam, CardBody ortam, CardFooter ortam | atlanan ProfileScreen,ProfileCard
baslik-degisti: headline degisti | yeniden ProfileScreen yerel, ProfileCard ebeveyn, CardTitle ebeveyn | atlanan CardBody,CardFooter
yazi-rengi-degisti: foregroundStyle degisti | yeniden CardBody ortam | atlanan ProfileScreen,ProfileCard,CardTitle,CardFooter
soluk-yapildi: isDimmed degisti | yeniden ProfileScreen yerel, ProfileCard ebeveyn | atlanan CardTitle,CardBody,CardFooter
yerel: 2
ebeveyn: 3
gozlem: 0
ortam: 4
```

Dört değişiklik dokuz yeniden hesaplama üretti: iki yerel, üç ebeveyn, dört ortam. `yazi-tipi-degisti` satırında üç görünüm ortam tetiğiyle yeniden hesaplandı, ekran ile kart atlandı; kart `font` değerini okumuyor, yalnız altındakilere ortamla ulaştırıyor. `baslik-degisti` satırında değerin sahibi olan ekran yerel; kart ile başlık aynı değeri parametre olarak aldıkları için ebeveyn; gövde metni ve alt bilgi başlığı okumadığından atlandı. `soluk-yapildi` satırında ekran yerel, kart ebeveyn tetiğiyle yeniden hesaplandı: `isDimmed` değerini ekranın altında yalnız kartın gövdesi okur, içindeki metinler bu değerle ilgilenmez.

Aynı anahtarı iki farklı yoldan okumak da mümkündür, ama bu ağaç onu yapmıyor: `font` ortamdan, `headline` parametreyle gelir. İki yolun tetikleri ayrıdır. Ortam yolunda değeri veren, görünümün üstündeki bir değiştirici ya da sistemdir; ebeveyn yolunda değeri veren, doğrudan üst görünümdür.

**GV4.** Bu ölçüm `font`, `foregroundStyle`, `headline` ve `isDimmed` anahtarları için. `font` anahtarının değişmesi bir üst değiştiricinin yeni değer vermesi ya da sistem ayarı olarak okunur; hangisi olduğu ölçülmedi. Kart iki değeri `let` parametresi olarak aldığı için onların sahibi değildir; değer kartın kendi `@State` alanı olsaydı kart yerel tetikle yeniden hesaplanırdı, bu durum burada ölçülmedi. Bu dersin iki ağacında da değerlerin sahibi olarak eklenen ekran bir model varsayımıdır; kaynak parçalar yalnız görünümlerin parametrelerini gösterir.

#### Değiştirici Sırası

Bir değiştirici sarmalayıcı döndürdüğü için sıra anlam taşır: sonra gelen değiştirici, öncekinin döndürdüğü görünümü sarar. `.padding()` önce, `.background()` sonra gelirse arka plan boşluk eklenmiş görünümü kaplar; sıra tersine çevrilirse arka plan yalnız metni kaplar, boşluk dışarıda kalır. Aşağıdaki iki parça aynı iki değiştiriciyi iki sırayla verir.

```swift
// Kaynak parca: padding once, background sonra
import SwiftUI

struct PaddedBadge: View {
    var body: some View {
        Text("Yeni")
            .padding()
            .background(Color.yellow)
    }
}
```

```swift
// Kaynak parca: background once, padding sonra
import SwiftUI

struct TightBadge: View {
    var body: some View {
        Text("Yeni")
            .background(Color.yellow)
            .padding()
    }
}
```

İlkinde sarı alan metnin çevresindeki boşluğu da kaplar, ikincisinde yalnız metnin kendisini. Bu fark bir yeniden hesaplama farkı değildir; iki görünümde de aynı anahtarlar okunur ve aynı değişiklikler aynı görünümleri yeniden hesaplatır. Sıra, görünüşü belirler, tetiği belirlemez. Bu yüzden ağaçta `padding` ya da `background` gibi değiştiriciler düğüm değildir; düğümler, değerleri okuyan görünümlerdir. Model, çerçevenin bu kuralını kurar: görünüşü değiştiren bir değiştirici, okuduğu değer ağaçta bir anahtarsa devreye girer; aksi halde modelde görünmez.

İki parça da KAYNAK'tır; yerleşim sonuçları platformun çizim motoruna aittir ve ölçülmedi. Sarı alanın hangisinde nereye kadar uzandığı, bu derste kaynak okumasıdır.

**GV5.** Değiştirici sırası bu derste yalnız iki parçada ve yalnız `padding` ile `background` için okundu. Üç ve daha çok değiştiricinin sıra kombinasyonları, `frame` ve `offset` gibi boyutu etkileyenlerin sırası ölçülmedi.

#### Sınırlayıcı Ölçüm

Ortam değeri alt ağaçtaki her görünüme ulaşır, ama yalnız onu okuyan görünüm değer değişince yeniden hesaplanır. Bir çocuk kendi yazı tipini koyduğunda, kapsayıcının `.font` değerini artık kullanmaz. Aşağıdaki ağaç bunu kurar: `CardTitle` ortamdan yazı tipini okur, `CardNote` ise kendi `.font` değiştiricisini koyduğu için artık `font` anahtarını okumaz, yalnız `note` değerini okur.

```swift
// main.swift — kendi fontunu koyan gorunum ortam anahtarini okumaz
let storage: [String: Storage] = [
    "font": .environment,
    "headline": .state(owner: "ProfileScreen"),
    "note": .state(owner: "ProfileScreen"),
]

let screen = ViewTree(nodes: [
    ViewNode("ProfileScreen", parent: nil, reads: ["headline", "note"]),
    ViewNode("ProfileCard", parent: "ProfileScreen", reads: ["headline", "note"]),
    ViewNode("CardTitle", parent: "ProfileCard", reads: ["headline", "font"]),
    ViewNode("CardNote", parent: "ProfileCard", reads: ["note"]),
], storage: storage)

let recomputes = [
    screen.change("yazi-tipi-degisti", key: "font"),
    screen.change("not-degisti", key: "note"),
].flatMap { $0 }

countCauses(recomputes)
```

```
yazi-tipi-degisti: font degisti | yeniden CardTitle ortam | atlanan ProfileScreen,ProfileCard,CardNote
not-degisti: note degisti | yeniden ProfileScreen yerel, ProfileCard ebeveyn, CardNote ebeveyn | atlanan CardTitle
yerel: 1
ebeveyn: 2
gozlem: 0
ortam: 1
```

`yazi-tipi-degisti` satırında `CardNote` atlanan listesinde: kapsayıcının yazı tipi değişti, ama not kendi yazı tipini koyduğundan o değeri okumuyor. `not-degisti` satırında değerin sahibi olan ekran yerel, kart ile not ebeveyn; `CardTitle` notu okumadığından atlandı. Yayılan bir değiştirici bu yüzden bir garanti değildir: ortam değerini okumayan, ortam değeri değişince yeniden hesaplanmaz.

Kaynakta karşılığı basittir. Not, kapsayıcıdan gelen yazı tipini kendi değiştiricisiyle ezer; bu, bir ortam anahtarını tüketmeyi bırakmaktır.

```swift
// Kaynak parca: kendi font'unu koyan gorunum kapsayicinin fontunu kullanmaz
import SwiftUI

struct CardNote: View {
    let note: String

    var body: some View {
        Text(note)
            .font(.caption)
    }
}
```

Bu da KAYNAK'tır. Çerçevenin kapsayıcıdan gelen değeri ezme sırasını, yani en yakın değiştiricinin kazandığını bu derste ölçmedik; yalnız modeldeki okuma kümesinin sonucunu ölçtük.

**GV6.** Bu ölçüm tek bir ortam anahtarı ve tek bir görünüm için. Ezmenin kısmi olduğu durum, örneğin kendi yazı tipi boyutunu koyup kapsayıcının ağırlığını kullanan bir görünüm, ölçülmedi.

#### Özet

- Değiştirici, görünümü değiştirmez; yeni bir görünüm döndürür ve sonra gelen değiştirici öncekini sarar.
- Kapsayıcıya konan yazı tipi ve renk, alt ağaca ortam değeri olarak yayılır; okuyan her görünüm ortam tetiğiyle yeniden hesaplanır.
- Ebeveynin parametreyle verdiği değer ortam değildir; ebeveyn tetiği üretir.
- Değiştirici sırası görünüşü belirler, tetiği belirlemez.
- Kendi yazı tipini koyan görünüm ortam anahtarını okumaz ve ortam değişince atlanır.
- Bu dersin tetiği: **ortam**.

#### Sonraki Adım

Bu derste değer ağaçta yukarıdan aşağıya aktı ve okuyan alıp kullandı. Sıradaki derste aynı akış görünümleri yan yana ya da üst üste dizen yığıtlarda aralık ve hizalama olarak karşımıza çıkar: bir yığıtın aralığını belirleyen değer ebeveynin durumudur. Soru aynı kalır, ama bu kez tetik, çocukları dizen kapsayıcının kendi değerinden gelir.

### Yığıt Düzenleri

Source: https://academia.sh/tr/kurslar/swiftui/yigit-duzenleri

Yığıtın aralık ve eksen değerlerinin ebeveyn durumundan çocuklara indiğinin, yön değerinin ortamdan okunduğunun ve derinlik yığıtında kardeş değerinin okunamadığının ölçümü.

Önceki derste bir değiştiricinin etkisinin alt ağaca ortam değeri olarak aktığını ölçtük. Bu derste aynı ağaçta çocukları dizen kapsayıcıya bakıyoruz: yığıt. Yığıt, çocuklarını yatayda, dikeyde ya da üst üste dizer ve aralarındaki boşluğu, hizalamayı kendi parametreleriyle belirler. Bu parametreler yığıtın sahibi olan görünümün durumundan gelir. Soru aynı kalır: **bu yeniden hesaplamayı ne tetikledi?** Bu derste bakılan tetik, **ebeveyn**.

```swift
// 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, açılış dersindekiyle aynıdır. Bu dersin ağacı bir araç çubuğudur: çubuğu kuran ekran, çubuğu tutan yığıt ve içindeki simge, etiket ve rozet.

#### Üç Yığıt, Üç Eksen

Yatay yığıt çocukları soldan sağa dizer, dikey yığıt yukarıdan aşağıya, derinlik yığıtı ise aynı alana üst üste koyar. İlk ikisinde aralık çocuklar arasındaki boşluğu, hizalama çocukların çapraz eksendeki konumunu belirler; üçüncüsünde yalnız hizalama vardır, çünkü çocuklar yan yana değil üst üste durur. Aşağıdaki parça üçünü bir arada gösterir.

```swift
// Kaynak parca: yatay, dikey ve derinlik yigiti
import SwiftUI

struct ToolbarStack: View {
    let spacing: Double
    let isVertical: Bool

    var body: some View {
        VStack(spacing: spacing) {
            HStack(spacing: spacing) {
                Image(systemName: "pencil")
                Text("Duzenle")
            }
            ZStack(alignment: isVertical ? .topTrailing : .bottomTrailing) {
                Image(systemName: "bell")
                Text("3")
            }
        }
    }
}
```

`spacing` ve `isVertical` saklanan değer değil, parametredir; yığıtı kuran görünüm ikisini de bir üst görünümden almıştır. Rozet ekseni okur: hizalaması `isVertical` değerine göre seçilir. Yığıtın içindeki çocuklar ise aralığı kendileri okumaz, yığıt aralığı çocukların arasına koyar. Bu ders modeli biraz sadeleştirir: yığıtın aralığına uyan her çocuk, aralığın değişmesiyle yeniden hesaplanan bir okuyucu sayılır. Bunun yaptığı şey, ebeveyn tetiğini görünür kılmaktır; çerçevenin aralığı çocuklara gerçekte nasıl ilettiğini göstermez.

**GV7.** Bu ölçüm yığıtın aralık ve eksen değerlerini çocuk görünümlerin okuduğu değerler olarak modeller. Çerçevenin yığıt değişince çocukları yeniden hesaplayıp hesaplamadığı ya da yalnız yerleşimi güncelleyip güncellemediği ölçülmedi.

#### Yığıttan Çocuğa İnen Değer

Bir yığıtın aralığı ve ekseni, yığıtı kuran görünümün durumundan gelir. Çocuk bu değeri okuyorsa, değerin sahibi çocuğun bir atasıdır ve model tetiği ebeveyn olarak sınıflar. Aşağıdaki ağaçta iki değerin sahibi yığıtı kuran `ToolbarScreen`'dir; `ToolbarStack` ikisini de `let` parametresi olarak alır. Aralığı simge ve etiket, ekseni (`isVertical`) rozet okur. Yazı yönü ise ortam değeridir ve etiketle rozet tarafından okunur.

```swift
// main.swift — yigitin degerleri ebeveyn, yon degeri ortam tetigi uretir
let storage: [String: Storage] = [
    "spacing": .state(owner: "ToolbarScreen"),
    "isVertical": .state(owner: "ToolbarScreen"),
    "layoutDirection": .environment,
]

let screen = ViewTree(nodes: [
    ViewNode("ToolbarScreen", parent: nil, reads: ["spacing", "isVertical"]),
    ViewNode("ToolbarStack", parent: "ToolbarScreen", reads: ["spacing", "isVertical"]),
    ViewNode("ToolIcon", parent: "ToolbarStack", reads: ["spacing"]),
    ViewNode("ToolLabel", parent: "ToolbarStack", reads: ["spacing", "layoutDirection"]),
    ViewNode("ToolBadge", parent: "ToolbarStack", reads: ["isVertical", "layoutDirection"]),
], storage: storage)

let recomputes = [
    screen.change("aralik-degisti", key: "spacing"),
    screen.change("eksen-degisti", key: "isVertical"),
    screen.change("yon-degisti", key: "layoutDirection"),
].flatMap { $0 }

countCauses(recomputes)
```

```
aralik-degisti: spacing degisti | yeniden ToolbarScreen yerel, ToolbarStack ebeveyn, ToolIcon ebeveyn, ToolLabel ebeveyn | atlanan ToolBadge
eksen-degisti: isVertical degisti | yeniden ToolbarScreen yerel, ToolbarStack ebeveyn, ToolBadge ebeveyn | atlanan ToolIcon,ToolLabel
yon-degisti: layoutDirection degisti | yeniden ToolLabel ortam, ToolBadge ortam | atlanan ToolbarScreen,ToolbarStack,ToolIcon
yerel: 2
ebeveyn: 5
gozlem: 0
ortam: 2
```

`aralik-degisti` satırında değerin sahibi olan ekran yerel; yığıt, simge ve etiket ebeveyn tetiğiyle yeniden hesaplandı; rozet aralığı okumadığından atlandı. `eksen-degisti` satırında ekran yine yerel, yığıt ile rozet ebeveyn tetiğiyle yeniden hesaplandı; simge ve etiket atlandı. `yon-degisti` satırında ekran ile yığıt atlandı, etiket ve rozet ortam tetiğiyle yeniden hesaplandı: yazı yönü yığıtı kuran ekranın durumu değil, ağaca dışarıdan giren bir değerdir.

Sayaç da aynı hikâyeyi özetler: iki yerel, beş ebeveyn, iki ortam. Beş ebeveyn tetiği, değerlerin sahibinin tek bir değer değiştirmesinin kaç çocuğu etkileyebileceğini gösterir. Aralığı değiştiren bir görünüm, aralığı okuyan her çocuğu da yeniden hesaplatır.

**GV8.** Bu ölçüm `spacing`, `isVertical` ve `layoutDirection` için. Bir değerin hem yığıta hem de çocuğa parametre olarak geçtiği, aynı anahtarın iki yoldan okunduğu durum ölçülmedi. Değerlerin sahibi olarak eklenen ekran bir model varsayımıdır; kaynak parça yalnız yığıtın parametrelerini gösterir.

#### Yön ve Eksen Seçimi

Yığıtın eksenini çalışma anında seçmek mümkündür: yatay ve dikey düzen arasında geçiş yapan bir düzen değeri, çocukları aynı kalırken eksenini değiştirir. Aşağıdaki parça bunu kaynak olarak gösterir.

```swift
// Kaynak parca: ekseni calisma aninda secen duzen
import SwiftUI

struct AdaptiveToolbar: View {
    let isVertical: Bool

    var body: some View {
        let layout = isVertical ? AnyLayout(VStackLayout()) : AnyLayout(HStackLayout())
        layout {
            Image(systemName: "pencil")
            Text("Duzenle")
        }
    }
}
```

`isVertical` bu görünümün `let` parametresidir, değerin sahibi onu kuran görünümdür; değer değişince yeni bir düzen değeri kurulur ve çocuklar aynı kalır. Modelde bu, `isVertical` anahtarının değişmesiyle aynıdır: değerin sahibi `ToolbarScreen` yerel tetikle, yığıt ve eksene bağlı çocuklar ebeveyn tetiğiyle yeniden hesaplanır. Yön ise ortamdan gelir ve düzen değeriyle ilgisi yoktur; sağdan sola yazılan bir dilde yatay yığıt çocuklarını tersten dizer, ama bu, yığıtın sahibinin kararı değildir.

**GV9.** Eksen seçimi bu derste yalnız kaynak parçada ve `isVertical` anahtarının modeldeki karşılığıyla ele alındı. Düzen değerinin değişmesinin çocukların kimliğini koruyup korumadığı ölçülmedi.

#### Sınırlayıcı Ölçüm

Derinlik yığıtında çocuklar aynı alanı paylaşır, ama kaynakta biri ötekinin `@State` değerine ulaşamaz: `@State` görünüme özeldir ve kardeş onu ne okuyabilir ne yazabilir. Bu kısıt yığıttan değil, `@State`'in görünüme özel olmasından gelir; yatay ya da dikey yığıttaki kardeşler için de aynıdır. Model bu kaynak kısıtını kendi kuralıyla taklit eder: değerin sahibi okuyanın atası değilse okuma `kural-disi` sayılır. Aşağıdaki ağaç kaynakta yazılamayacak bu okumayı modelde bilerek kurar: fotoğraf katmanı kendi `photoName` değerinin sahibidir, bindirme katmanı da bu değeri okuyormuş gibi modellenir.

```swift
// main.swift — derinlik yiginindaki kardes, digerinin degerini okuyamaz
let storage: [String: Storage] = [
    "photoName": .state(owner: "PhotoLayer"),
    "caption": .state(owner: "PhotoOverlay"),
]

let screen = ViewTree(nodes: [
    ViewNode("PhotoStack", parent: nil, reads: []),
    ViewNode("PhotoLayer", parent: "PhotoStack", reads: ["photoName"]),
    ViewNode("PhotoOverlay", parent: "PhotoStack", reads: ["caption", "photoName"]),
], storage: storage)

let recomputes = [
    screen.change("foto-degisti", key: "photoName"),
    screen.change("yazi-degisti", key: "caption"),
].flatMap { $0 }

countCauses(recomputes)
```

```
foto-degisti: photoName degisti | yeniden PhotoLayer yerel, PhotoOverlay kural-disi | atlanan PhotoStack
yazi-degisti: caption degisti | yeniden PhotoOverlay yerel | atlanan PhotoStack,PhotoLayer
yerel: 2
ebeveyn: 0
gozlem: 0
ortam: 0
```

`foto-degisti` satırında fotoğraf katmanı yerel, bindirme `kural-disi` olarak işaretlendi. Model, değerin sahibinin okuyanın atası olmadığını görür ve bu okumayı kural dışı sayar; bu bir çerçeve davranışının ölçümü değil, modelin kurulmaması gereken bir okumayı yakalamasıdır. İki katmanın aynı değeri kullanması gerekiyorsa değer ortak ataya, burada `PhotoStack`'e taşınır; o zaman iki katman da onu ebeveyn tetiğiyle okur. `yazi-degisti` satırında yalnız bindirme yeniden hesaplandı.

Sayaçta kural dışı tetik görünmez, çünkü `countCauses` yalnız dört tetiği sayar: yerel iki, ötekiler sıfır. Bu, sayaçtaki toplamın her zaman yeniden hesaplama sayısına eşit olmadığını gösterir; sayılmayan tetikler ayrıca okunmalıdır.

**GV10.** Bu ölçüm iki kardeş görünüm ve tek bir paylaşılan anahtar için. Çerçevenin kardeşler arasında değer aktarma yollarını, örneğin tercih anahtarlarını, ölçmedik.

#### Özet

- Yığıt çocuklarını yatayda, dikeyde ya da üst üste dizer; aralık ve hizalama yığıtın parametreleridir.
- Yığıtı kuran görünümün durumundan gelen aralık ve eksen, yığıt ve çocuklar için ebeveyn tetiği üretir.
- Yazı yönü ortamdan gelir; yığıtın sahibinin durumu olmadığı için ortam tetiği üretir.
- Eksen seçimi, yığıtın sahibinin tek bir değeriyle yığıtı ve ona bağlı çocukları etkiler.
- Kardeşler, aynı alanı paylaşan derinlik yığıtında bile birbirinin `@State` değerini okuyamaz; kısıt `@State`'in görünüme özel olmasından gelir, model bu okumayı kural dışı sınıflar.
- Bu dersin tetiği: **ebeveyn**.

#### Sonraki Adım

Bu derste yığıtın aralığı ve ekseni, sahibi olan ebeveynden çocuklara indi. Sıradaki derste aynı iniş boyutlar için işler: bir ebeveyn çocuğuna ne kadar yer önerdiğini nasıl belirler ve bu öneri hangi değere bağlıdır. Soru aynı kalır, ama bu kez değer bir aralık değil, bir boyut önerisidir.

### Düzen Sistemi

Source: https://academia.sh/tr/kurslar/swiftui/duzen-sistemi

Önerilen boyutu belirleyen ebeveyn değerinin çocuklara ebeveyn tetiği olarak indiğinin, yazı boyutu tercihinin ortamdan okunduğunun ve boyut önerisinin ölçülen bir değer olmadığının ölçümü.

Önceki derste yığıtın aralık ve ekseni, sahibi olan ebeveynden çocuklara indi. Bu derste aynı inişi boyutlar için izliyoruz. Düzen sisteminde üst görünüm, çocuğuna ne kadar yer önerdiğini söyler; çocuk bu öneriden kendi boyutunu seçer; üst görünüm de çocuğu seçtiği boyutla yerleştirir. Önerinin kendisi bir ebeveyn kararıdır, ama bu kararı belirleyen bir değer çoğu zaman ebeveynin durumunda durur. Soru aynı kalır: **bu yeniden hesaplamayı ne tetikledi?** Bu derste bakılan tetik, **ebeveyn**.

```swift
// 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, açılış dersindekiyle aynıdır. Bu dersin ağacı bir özet panelidir: panelin kendisi, bir başlık, bir grafik ve bir not.

#### Öneri, Seçim, Yerleştirme

Düzen üç adımda ilerler. Ebeveyn çocuğa bir boyut önerir; çocuk bu öneriyi kendi içeriğine göre yorumlayıp bir boyut seçer; ebeveyn de çocuğu seçilen boyutla kendi alanında bir yere koyar. Çocuk öneriyi kabul edebilir, küçük kalabilir ya da öneriden büyük bir boyut isteyebilir. Aşağıdaki parça adımları kaynak olarak gösterir.

```swift
// Kaynak parca: oneri, secim ve yerlestirme
import SwiftUI

struct SummaryPanel: View {
    @State private var isCompact = false

    var body: some View {
        VStack {
            Text("Ozet")
                .layoutPriority(isCompact ? 0 : 1)
            Rectangle()
                .frame(maxWidth: .infinity, minHeight: isCompact ? 40 : 120)
            Text("Not")
                .lineLimit(2)
        }
    }
}
```

`isCompact` panelin kendi durumudur ve iki yerde yer önerisini etkiler: başlığın düzen önceliği ile grafiğin asgari yüksekliği. Notun satır sınırı sabittir. `layoutPriority` ise yer daraldığında hangi çocuğun önce yer alacağını söyler; yüksek öncelikli çocuk önce boyutunu seçer, kalan yer öteki çocuklara bölüşülür. Bu parça KAYNAK'tır ve boyutların sayı olarak ne çıktığı bu derste ölçülmedi.

**GV11.** Bu ölçüm düzenin üç adımını anlatır ama boyut hesabı yapmaz. Önerilen boyutun hangi sayıya dönüştüğü, `layoutPriority` değerlerinin bölüşümü ve seçilen boyutların yerleştirmesi ölçülmedi.

#### Önerinin Dayandığı Değer

Öneriyi belirleyen değer ebeveynin durumundaysa, çocuğun bunu okuması ebeveyn tetiği üretir. Aşağıdaki ağaçta `isCompact` ve `density` panelin değerleridir; başlık ve grafik bunları okur. Yazı boyutu tercihi (`dynamicTypeSize`) ise ortamdan gelir ve başlık ile notun yer ihtiyacını etkiler.

```swift
// main.swift — onerilen boyutu belirleyen deger ebeveyn, yazi boyutu ortam
let storage: [String: Storage] = [
    "isCompact": .state(owner: "SummaryPanel"),
    "density": .state(owner: "SummaryPanel"),
    "dynamicTypeSize": .environment,
]

let screen = ViewTree(nodes: [
    ViewNode("SummaryPanel", parent: nil, reads: ["isCompact", "density"]),
    ViewNode("SummaryTitle", parent: "SummaryPanel", reads: ["isCompact", "dynamicTypeSize"]),
    ViewNode("SummaryChart", parent: "SummaryPanel", reads: ["isCompact", "density"]),
    ViewNode("SummaryNote", parent: "SummaryPanel", reads: ["dynamicTypeSize"]),
], storage: storage)

let recomputes = [
    screen.change("kompakt-oldu", key: "isCompact"),
    screen.change("yogunluk-degisti", key: "density"),
    screen.change("yazi-boyutu-degisti", key: "dynamicTypeSize"),
].flatMap { $0 }

countCauses(recomputes)
```

```
kompakt-oldu: isCompact degisti | yeniden SummaryPanel yerel, SummaryTitle ebeveyn, SummaryChart ebeveyn | atlanan SummaryNote
yogunluk-degisti: density degisti | yeniden SummaryPanel yerel, SummaryChart ebeveyn | atlanan SummaryTitle,SummaryNote
yazi-boyutu-degisti: dynamicTypeSize degisti | yeniden SummaryTitle ortam, SummaryNote ortam | atlanan SummaryPanel,SummaryChart
yerel: 2
ebeveyn: 3
gozlem: 0
ortam: 2
```

`kompakt-oldu` satırında panel yerel, başlık ve grafik ebeveyn tetiğiyle yeniden hesaplandı; not atlandı. `yogunluk-degisti` satırında yalnız grafik okuduğu için panel yerel, grafik ebeveyn. `yazi-boyutu-degisti` satırında panel ve grafik atlandı, başlık ile not ortam tetiğiyle yeniden hesaplandı. Yazı boyutu tercihini kullanıcı sistem ayarından değiştirir; hiçbir ebeveynin durumu bu değerin sahibi değildir.

Notun `isCompact` değerini okumaması kaynak parçayla uyumludur: satır sınırı sabit bir sayıdır. Okuma kümesi kaynak parçanın okumasından türetilir; kaynakta bulunmayan okuma varsayımdır ve GV12'de belirtilir; not yalnız yazı boyutuna bağlı kaldığı için `kompakt-oldu` satırında atlanır.

**GV12.** Bu ölçüm `isCompact`, `density` ve `dynamicTypeSize` için. Kaynak parçada görünmeyen `density` değerinin panelin durumu olduğu ve yalnız grafiğin onu okuduğu varsayıldı; bu bir ölçüm kuralı değil, ağaç tanımıdır.

#### Sınırlayıcı Ölçüm

Önerilen boyut bir durum değeri değildir. Ebeveyn ile çocuk arasındaki bir görüşmedir: ebeveyn önerir, çocuk seçer ve sonuç yerleşimde ortaya çıkar. Modelde bu görüşmenin bir anahtarı yoktur. Grafik `proposedWidth` değerini okuyor gibi yazılsa bile, model bunu saklama tablosunda bulamaz ve tetiği ölçüm dışı sayar.

```swift
// main.swift — onerilen genislik tabloda yoksa tetik olcum disidir
let storage: [String: Storage] = [
    "isCompact": .state(owner: "SummaryPanel"),
]

let screen = ViewTree(nodes: [
    ViewNode("SummaryPanel", parent: nil, reads: ["isCompact"]),
    ViewNode("SummaryChart", parent: "SummaryPanel", reads: ["isCompact", "proposedWidth"]),
    ViewNode("SummaryNote", parent: "SummaryPanel", reads: []),
], storage: storage)

let recomputes = [
    screen.change("genislik-onerildi", key: "proposedWidth"),
    screen.change("kompakt-oldu", key: "isCompact"),
].flatMap { $0 }

countCauses(recomputes)
```

```
genislik-onerildi: proposedWidth degisti | yeniden SummaryChart olcum-disi | atlanan SummaryPanel,SummaryNote
kompakt-oldu: isCompact degisti | yeniden SummaryPanel yerel, SummaryChart ebeveyn | atlanan SummaryNote
yerel: 1
ebeveyn: 1
gozlem: 0
ortam: 0
```

`genislik-onerildi` satırı çekirdeğin ölçemediği satırdır: grafik değeri okuduğu için listelenir, ama satır `olcum-disi` yazar ve bir yeniden hesaplama iddiası değildir; sayaç da onu göstermez. Model burada bir hesap yapmaz: önerilen genişliğin sayı olarak ne olduğunu, grafiğin ne seçtiğini ya da paneldeki yerleşimi bilmez. Yalnız hangi görünümün hangi anahtarı okuduğunu ve anahtarın tabloda olup olmadığını söyler. `kompakt-oldu` satırı ise tabloda olan değer için sonucun nasıl değiştiğini gösterir.

**GV13.** Bu ölçüm boyut görüşmesini tek bir anahtara indirger ve o anahtarın tabloda olmadığı durumu inceler. Gerçek bir boyut değişiminin hangi görünümleri yeniden hesaplattığı, çerçevenin yerleşim geçişleri ve çocuğun öneriye verdiği yanıt ölçülmedi.

#### Özet

- Düzen üç adımdır: ebeveyn önerir, çocuk boyutunu seçer, ebeveyn çocuğu yerleştirir.
- Önerilen boyutu belirleyen değer ebeveynin durumundaysa, onu okuyan çocuk ebeveyn tetiğiyle yeniden hesaplanır.
- Yazı boyutu tercihi ortamdan gelir ve ortam tetiği üretir.
- `layoutPriority` yer daraldığında kimin önce boyut seçeceğini belirler; sayısal bölüşümü bu derste ölçülmedi.
- Önerilen boyut bir durum değeri değildir; modelde anahtarı yoksa tetik ölçüm dışı sınıflanır.
- Bu dersin tetiği: **ebeveyn**.

#### Sonraki Adım

Bu derste ebeveyn değeri, çocuğa önerilen boyutun dayanağıydı. Sıradaki derste aynı soruyu ızgara için soruyoruz: bir ızgaranın kaç sütun gösterdiğini ne belirler. Cevabın bir kısmı ortamdan gelen bir boyut sınıfıdır; bu yüzden tetik de ortam olacaktır.

### Izgara ve Geometri

Source: https://academia.sh/tr/kurslar/swiftui/izgara-ve-geometri

Boyut sınıfının ızgara sütunlarını ortam değeri olarak belirlediğinin, ızgara verisinin ebeveynden indiğinin ve ölçülen boyutun durum olmadığı için ölçüm dışı kaldığının ölçümü.

Önceki derste önerilen boyutu belirleyen değer ebeveynin durumuydu. Bu derste bir ızgaranın kaç sütun gösterdiğine bakıyoruz ve cevabın bir kısmı ağacın dışından geliyor: ekranın yatay boyut sınıfı. Bir ızgara sütun tanımlarından hücre düzenini kurar; sütun sayısı ise çoğunlukla ızgarayı kuran görünümün ortamdan okuduğu bir değere bağlıdır. Soru aynı kalır: **bu yeniden hesaplamayı ne tetikledi?** Bu derste bakılan tetik, **ortam**.

```swift
// 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, açılış dersindekiyle aynıdır. Bu dersin ağacı bir galeridir: galeri ekranı, ızgara, hücre ve alt yazı.

#### Sütun Tanımından Izgaraya

Tembel dikey ızgara, sütun tanımlarının listesinden düzenini kurar. Üç esnek sütun, hücreleri üç sütuna dizer; tanım listesi kısaldığında aynı hücreler daha az sütuna yayılır. Aşağıdaki parça sütun sayısını yatay boyut sınıfına bağlar.

```swift
// Kaynak parca: sutun sayisini boyut sinifi belirler
import SwiftUI

struct GalleryGrid: View {
    @Environment(\.horizontalSizeClass) private var sizeClass
    let names: [String]

    var body: some View {
        let count = sizeClass == .compact ? 2 : 4
        let columns = Array(repeating: GridItem(.flexible()), count: count)
        LazyVGrid(columns: columns) {
            ForEach(names, id: \.self) { name in
                Text(name)
            }
        }
    }
}
```

`sizeClass` ortamdan okunur ve ızgaranın sütun sayısını belirler. Ekran döndüğünde ya da pencere boyutu değiştiğinde değer değişir ve ızgaranın gövdesi yeniden hesaplanır. `names` ise ızgarayı kuran görünümden gelen bir parametredir.

**GV14.** Bu ölçüm yatay boyut sınıfını tek bir ortam anahtarı olarak ele alır. Boyut sınıfının hangi eşikte değiştiği ve değerin hangi pencere genişliğine karşılık geldiği ölçülmedi.

#### Boyut Sınıfı ve Izgara Verisi

Aşağıdaki ağaçta galeri ekranı bir süzgeç etiketinin sahibidir; ızgara ve hücre bu etiketi okur. Izgara aynı zamanda kendi fotoğraf sayısının sahibidir ve boyut sınıfını ortamdan okur. Alt yazı da boyut sınıfını okur, çünkü küçük ekranda tek satıra sığması gerekir.

```swift
// main.swift — boyut sinifi ortam, izgara verisi ebeveyn, fotograf sayisi yerel
let storage: [String: Storage] = [
    "horizontalSizeClass": .environment,
    "filterTag": .state(owner: "GalleryScreen"),
    "photoCount": .state(owner: "GalleryGrid"),
]

let screen = ViewTree(nodes: [
    ViewNode("GalleryScreen", parent: nil, reads: ["filterTag"]),
    ViewNode("GalleryGrid", parent: "GalleryScreen", reads: ["horizontalSizeClass", "photoCount", "filterTag"]),
    ViewNode("GalleryCell", parent: "GalleryGrid", reads: ["filterTag"]),
    ViewNode("GalleryCaption", parent: "GalleryGrid", reads: ["horizontalSizeClass"]),
], storage: storage)

let recomputes = [
    screen.change("sinif-degisti", key: "horizontalSizeClass"),
    screen.change("etiket-degisti", key: "filterTag"),
    screen.change("adet-degisti", key: "photoCount"),
].flatMap { $0 }

countCauses(recomputes)
```

```
sinif-degisti: horizontalSizeClass degisti | yeniden GalleryGrid ortam, GalleryCaption ortam | atlanan GalleryScreen,GalleryCell
etiket-degisti: filterTag degisti | yeniden GalleryScreen yerel, GalleryGrid ebeveyn, GalleryCell ebeveyn | atlanan GalleryCaption
adet-degisti: photoCount degisti | yeniden GalleryGrid yerel | atlanan GalleryScreen,GalleryCell,GalleryCaption
yerel: 2
ebeveyn: 2
gozlem: 0
ortam: 2
```

`sinif-degisti` satırında ızgara ve alt yazı ortam tetiğiyle yeniden hesaplandı; ekran ve hücre atlandı. Hücre sütun sayısını bilmez, yalnız kendi içeriğini çizer. `etiket-degisti` satırında ekran yerel, ızgara ve hücre ebeveyn tetiğiyle yeniden hesaplandı. `adet-degisti` satırında yalnız ızgara yerel tetikle yeniden hesaplandı: fotoğraf sayısının sahibi ızgaranın kendisidir.

Üç tetik üç farklı yoldan geldi: ortam, ebeveyn ve yerel. Izgara en çok okuyan görünümdür ve bu yüzden en çok yeniden hesaplanan görünümdür; her değişiklikte ızgara listede yer aldı, ekran ve hücre ise yalnız süzgeç değiştiğinde.

**GV15.** Bu ölçüm `horizontalSizeClass`, `filterTag` ve `photoCount` için. Izgaranın sütun sayısı değişince hücrelerin yeniden hesaplanıp hesaplanmadığı, yani çerçevenin hücre kimliğini nasıl koruduğu ölçülmedi.

#### Geometri Okuyucu

Geometri okuyucu, kendisine önerilen boyutu ve koordinatları içeriğine verir. İçerik bu boyuta göre kendini çizer. Aşağıdaki parça okuyucunun verdiği genişliğe göre iki dala ayrılır.

```swift
// Kaynak parca: geometri okuyucunun verdigi genislige gore cizim
import SwiftUI

struct AdaptiveBanner: View {
    var body: some View {
        GeometryReader { proxy in
            if proxy.size.width > 400 {
                Text("Genis gorunum")
            } else {
                Text("Dar gorunum")
            }
        }
    }
}
```

Okuyucunun verdiği `proxy.size` bir durum değeri değildir: kimse onu kaydetmez ve kimse onun sahibi değildir. Değer, düzen geçişinde ebeveynin önerdiği boyuttan hesaplanır. Bu yüzden modelde geometrinin bir saklama yeri yoktur.

#### Sınırlayıcı Ölçüm

Aşağıdaki ağaçta okuyucu `readerSize` değerini okuyor gibi yazılmıştır, ama bu anahtar saklama tablosunda yoktur. Tabloda yalnız ekranın kendi modu vardır.

```swift
// main.swift — geometri okuyucunun boyutu tabloda yoksa tetik olcum disidir
let storage: [String: Storage] = [
    "mode": .state(owner: "ReaderScreen"),
]

let screen = ViewTree(nodes: [
    ViewNode("ReaderScreen", parent: nil, reads: ["mode"]),
    ViewNode("SizeReader", parent: "ReaderScreen", reads: ["mode", "readerSize"]),
    ViewNode("ReaderLabel", parent: "ReaderScreen", reads: ["mode"]),
], storage: storage)

let recomputes = [
    screen.change("boyut-olculdu", key: "readerSize"),
    screen.change("mod-degisti", key: "mode"),
].flatMap { $0 }

countCauses(recomputes)
```

```
boyut-olculdu: readerSize degisti | yeniden SizeReader olcum-disi | atlanan ReaderScreen,ReaderLabel
mod-degisti: mode degisti | yeniden ReaderScreen yerel, SizeReader ebeveyn, ReaderLabel ebeveyn | atlanan yok
yerel: 1
ebeveyn: 2
gozlem: 0
ortam: 0
```

`boyut-olculdu` satırı çekirdeğin ölçemediği satırdır: okuyucu değeri okuduğu için listelenir, ama satır `olcum-disi` yazar ve bir yeniden hesaplama iddiası değildir; ekran ve etiket atlanan olarak görünür. Model burada hiçbir boyut hesabı yapmaz: yalnız okunan anahtarın tabloda olmadığını görür ve tetiği ölçüm dışı sayar. `mod-degisti` satırında ise tabloda olan değer için ekran yerel, okuyucu ve etiket ebeveyn tetiğiyle yeniden hesaplandı.

**GV16.** Bu ölçüm geometriyi tek bir tablo dışı anahtara indirger. Gerçek bir geometri değişiminin hangi görünümleri yeniden hesaplattığı, okuyucunun düzen geçişinde kaç kez çağrıldığı ve koordinat alanlarının davranışı ölçülmedi.

#### Özet

- Izgara, sütun tanımlarından hücre düzenini kurar; sütun sayısı sık sık bir ortam değerine bağlıdır.
- Yatay boyut sınıfı ortamdan okunur ve okuyan ızgara ile alt yazı ortam tetiğiyle yeniden hesaplanır.
- Izgara verisini veren ebeveyn durumu, hücreler için ebeveyn tetiği üretir.
- Izgaranın kendi sahip olduğu değer yerel tetik üretir.
- Geometri okuyucunun verdiği boyut durum değildir; modelde anahtarı yoksa tetik ölçüm dışı sınıflanır.
- Bu dersin tetiği: **ortam**.

#### Sonraki Adım

Bu derste ızgaranın verisi ve boyut sınıfı iki ayrı yoldan geldi. Sıradaki derste aynı soruyu liste için soruyoruz: bir listenin satırları, kendi ağacının dışında yaşayan bir modelin koleksiyonuna bağlandığında ne olur. Bu kez tetik, ağacın dışındaki bir modelden gelir.

### Listeler ve Formlar

Source: https://academia.sh/tr/kurslar/swiftui/listeler-ve-formlar

Modelin koleksiyonunu okuyan listenin gözlem tetiğiyle, form alanının ebeveyn durumuna bağlanmasının ebeveyn tetiğiyle yeniden hesaplandığının ve tek satırın özelliği değişince öteki satırların atlandığının ölçümü.

Önceki derste ızgaranın verisi ebeveynden, sütun sayısı ortamdan geldi. Bu derste bir listenin satırlarının kaynağına bakıyoruz: koleksiyon çoğu zaman ağacın dışında yaşayan bir modeldedir. Liste bu koleksiyonu okur ve her öğe için bir satır kurar. Form ise aynı listeye benzer ama içindeki alanlar, değerlerini bir üst görünümün durumuna bağlama ile bağlar. Soru aynı kalır: **bu yeniden hesaplamayı ne tetikledi?** Bu derste bakılan tetik, **gözlem**.

```swift
// 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, açılış dersindekiyle aynıdır. Bu dersin ağacı bir not kutusudur: ekran, form, liste ve iki satır.

#### Koleksiyondan Satıra

Liste, öğeleri kimlikle ayırt edilen bir koleksiyondan satırlarını kurar. Kimlik, bir öğenin koleksiyonda yeri değişse bile aynı satıra karşılık gelmesini sağlar. Aşağıdaki parça modelin koleksiyonunu bir listeye bağlar.

```swift
// Kaynak parca: modelin koleksiyonundan liste
import SwiftUI
import Observation

@Observable
final class NoteStore {
    var notes: [Note] = []
}

@Observable
final class Note: Identifiable {
    let id: Int
    var title: String

    init(id: Int, title: String) {
        self.id = id
        self.title = title
    }
}

struct InboxList: View {
    let store: NoteStore

    var body: some View {
        List(store.notes) { note in
            Text(note.title)
        }
    }
}
```

`store.notes` listenin gövdesinde okunur; bu, gözlemlenen bir modelin değerini okumaktır. Koleksiyon değiştiğinde, yani bir not eklendiğinde ya da silindiğinde, listenin gövdesi yeniden hesaplanır. `Note` da `@Observable` bir sınıftır: her not kendi başına gözlemlenen bir nesnedir ve satır `note.title` değerini okuduğunda o notun başlığına bağlanır. Model, çerçevenin bağımlılık izleme kuralını kurar; çerçevenin kendisi değildir.

**GV17.** Bu ölçüm koleksiyonu tek bir `notes` anahtarı olarak modeller. Koleksiyonun bir öğesinin sırasının değişmesi ile öğe eklenmesinin farklı sonuç verip vermediği ölçülmedi.

#### Liste, Form ve Satır Durumu

Aşağıdaki ağaçta ekran, taslak başlığın sahibidir; form alanı bu değere bağlama ile bağlıdır. Liste modelin koleksiyonunu okur. İlk satırın başlığı o satırın `Note` nesnesinden gelir, açık ya da kapalı olması ise satırın kendi durumudur.

```swift
// main.swift — koleksiyon gozlem, form alani ebeveyn, satir durumu yerel
let storage: [String: Storage] = [
    "notes": .model(object: "NoteStore"),
    "draftTitle": .state(owner: "InboxScreen"),
    "noteTitle1": .model(object: "Note1"),
    "noteTitle2": .model(object: "Note2"),
    "expanded1": .state(owner: "NoteRow1"),
    "expanded2": .state(owner: "NoteRow2"),
]

let screen = ViewTree(nodes: [
    ViewNode("InboxScreen", parent: nil, reads: ["draftTitle"]),
    ViewNode("NoteForm", parent: "InboxScreen", reads: ["draftTitle"]),
    ViewNode("InboxList", parent: "InboxScreen", reads: ["notes"]),
    ViewNode("NoteRow1", parent: "InboxList", reads: ["noteTitle1", "expanded1"]),
    ViewNode("NoteRow2", parent: "InboxList", reads: ["noteTitle2", "expanded2"]),
], storage: storage)

let recomputes = [
    screen.change("koleksiyon-degisti", key: "notes"),
    screen.change("taslak-yazildi", key: "draftTitle"),
    screen.change("satir-acildi", key: "expanded1"),
].flatMap { $0 }

countCauses(recomputes)
```

```
koleksiyon-degisti: notes degisti | yeniden InboxList gozlem | atlanan InboxScreen,NoteForm,NoteRow1,NoteRow2
taslak-yazildi: draftTitle degisti | yeniden InboxScreen yerel, NoteForm ebeveyn | atlanan InboxList,NoteRow1,NoteRow2
satir-acildi: expanded1 degisti | yeniden NoteRow1 yerel | atlanan InboxScreen,NoteForm,InboxList,NoteRow2
yerel: 2
ebeveyn: 1
gozlem: 1
ortam: 0
```

`koleksiyon-degisti` satırında yalnız liste gözlem tetiğiyle yeniden hesaplandı; ekran, form ve satırlar atlandı. Satırlar koleksiyonu değil, kendi başlıklarını okuyor. `taslak-yazildi` satırında ekran yerel, form ebeveyn tetiğiyle yeniden hesaplandı: form alanı değerini ekranın durumuna bağlama ile bağlar ve ekranın durumu değişince alan da yeniden hesaplanır. `satir-acildi` satırında yalnız ilk satır yerel tetikle yeniden hesaplandı.

Üç değişiklik üç ayrı tetik üretti: gözlem, ebeveyn ve yerel. Modelin koleksiyonu değişince tek bir görünüm yeniden hesaplandı; bu, listenin koleksiyonu okuyan tek görünüm olmasının sonucudur.

**GV18.** Bu ölçüm `notes`, `draftTitle` ve `expanded1` için. Bağlama ile bağlı alanın değerini değiştirdiğinde ekranın mı yoksa alanın mı önce yeniden hesaplandığı ölçülmedi.

#### Form Alanı ve Bağlama

Form alanı, değerini okuyup değiştirebilmek için bir bağlamaya ihtiyaç duyar. Bağlama, değerin sahibine bir yol verir; alan yeni değeri bu yoldan yazar. Aşağıdaki parça bir metin alanını ekranın durumuna bağlar.

```swift
// Kaynak parca: form alani ebeveynin durumuna baglama ile baglanir
import SwiftUI

struct InboxScreen: View {
    @State private var draftTitle = ""

    var body: some View {
        NoteForm(title: $draftTitle)
    }
}

struct NoteForm: View {
    @Binding var title: String

    var body: some View {
        Form {
            TextField("Baslik", text: $title)
        }
    }
}
```

`$draftTitle` ifadesi, durumun kendisini değil ona giden bir yolu verir; form bu yolu `title` bağlaması olarak alır ve metin alanına iletir. Alan yazıldıkça durumun sahibi güncellenir ve sahibin gövdesi yeniden hesaplanır. Modelde bu, `draftTitle` anahtarının ekran tarafından sahiplenilip form tarafından okunmasıdır.

#### Sınırlayıcı Ölçüm

Bir listede bir satırın özelliği değişince öteki satırlar bu değeri okumaz. Aşağıdaki ağaçta liste koleksiyonu, her satır ise yalnız kendi `Note` nesnesinin başlığını okur.

```swift
// main.swift — tek satirin basligi degisince oteki satirlar atlanir
let storage: [String: Storage] = [
    "notes": .model(object: "NoteStore"),
    "noteTitle1": .model(object: "Note1"),
    "noteTitle2": .model(object: "Note2"),
    "noteTitle3": .model(object: "Note3"),
]

let screen = ViewTree(nodes: [
    ViewNode("InboxList", parent: nil, reads: ["notes"]),
    ViewNode("NoteRow1", parent: "InboxList", reads: ["noteTitle1"]),
    ViewNode("NoteRow2", parent: "InboxList", reads: ["noteTitle2"]),
    ViewNode("NoteRow3", parent: "InboxList", reads: ["noteTitle3"]),
], storage: storage)

let recomputes = [
    screen.change("baslik-degisti", key: "noteTitle2"),
    screen.change("not-eklendi", key: "notes"),
].flatMap { $0 }

countCauses(recomputes)
```

```
baslik-degisti: noteTitle2 degisti | yeniden NoteRow2 gozlem | atlanan InboxList,NoteRow1,NoteRow3
not-eklendi: notes degisti | yeniden InboxList gozlem | atlanan NoteRow1,NoteRow2,NoteRow3
yerel: 0
ebeveyn: 0
gozlem: 2
ortam: 0
```

`baslik-degisti` satırında yalnız ikinci satır gözlem tetiğiyle yeniden hesaplandı; liste ve öteki iki satır atlandı. `not-eklendi` satırında ise yalnız liste yeniden hesaplandı: satırların başlıkları değişmediği için hiçbir satır yeniden hesaplanmadı. Bu, bir satırın başlığının değişmesinin listenin yeniden hesaplanmasını gerektirmediğini gösterir.

Sayaç iki gözlem tetiği gösterir ve başka tetik yoktur. Başlığı ayrı bir anahtar yapan, `Note`'un sınıf olmasıdır: bir notun başlığını yazmak `notes` dizisini değiştirmez, dizi aynı nesnelere giden aynı başvuruları tutmaya devam eder. Büyük bir listede bunun anlamı şudur: bir satırı güncelleyen yazma yalnız o satırı okuyan görünümü etkiler.

**GV19.** Bu ölçüm üç satırlı bir listeyi ve her satırın tek bir başlık anahtarı okumasını modeller. Çerçevenin satır kimliğini koruma biçimi, kaydırma sırasında görünmeyen satırların davranışı ve büyük koleksiyonlarda ölçek ölçülmedi. `Note` bir `struct` olsaydı başlık yazması `notes` dizisine yazma sayılır ve ağaçta `notes` anahtarı değişirdi; bu modelleme burada kurulmadı.

#### Özet

- Liste, öğeleri kimlikle ayırt edilen bir koleksiyondan satırlarını kurar.
- Modelin koleksiyonu değişince, koleksiyonu okuyan liste gözlem tetiğiyle yeniden hesaplanır.
- Form alanı değerini bağlama ile ebeveynin durumuna bağlar; durum değişince alan ebeveyn tetiğiyle yeniden hesaplanır.
- Satırın kendi açık ya da kapalı durumu yerel tetik üretir.
- Bir satırın başlığı değişince yalnız o satırı okuyan görünüm yeniden hesaplanır; öteki satırlar atlanır.
- Bu dersin tetiği: **gözlem**.

#### Sonraki Adım

Bu derste tetik, ağacın dışındaki bir modelden geldi. Sıradaki derste görsel bileşenlere bakıyoruz: bir simgenin ya da şeklin rengi hangi değere bağlıdır. Cevap yine ortamdan gelen bir değerdir, bu kez renk şemasıdır.

### Görsel Bileşenler

Source: https://academia.sh/tr/kurslar/swiftui/gorsel-bilesenler

Renk şemasının simge rengini ortam değeri olarak belirlediğinin, şeklin dolgu oranının ebeveynden indiğinin, görselin yükleniyor durumunun yerel kaldığının ve statik görselin hiçbir değer okumadığının ölçümü.

Önceki derste tetik, ağacın dışındaki bir modelden geldi. Bu derste ağacın yapraklarına bakıyoruz: metin, simge, şekil ve görsel. Bu bileşenler başka görünümleri içermez ama değer okur: bir simgenin rengi, bir şeklin dolgusu ve bir görselin yüklenme durumu birer değere bağlıdır. Soru aynı kalır: **bu yeniden hesaplamayı ne tetikledi?** Bu derste bakılan tetik, **ortam**.

```swift
// 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, açılış dersindekiyle aynıdır. Bu dersin ağacı bir gösterge panelidir: paneli kuran ekran, panel, şekil, simge ve avatar.

#### Yaprak Bileşenler

Metin, simge, şekil ve görsel ağacın yapraklarıdır. Her biri kendi biçimini ya da içeriğini bir değerden alır. Aşağıdaki parça dördünü bir arada gösterir.

```swift
// Kaynak parca: metin, simge, sekil ve gorsel
import SwiftUI

struct MeterPanel: View {
    let fillRatio: Double

    var body: some View {
        VStack {
            Text("Doluluk")
            Image(systemName: "gauge")
            Capsule()
                .fill(Color.blue)
                .frame(width: 200 * fillRatio, height: 12)
            Image("logo")
        }
    }
}
```

Şeklin genişliği panelin `fillRatio` parametresinden gelir. Simge ise ön plan rengini ortamdan alır. `Image("logo")` bir varlık adıyla kurulan statik bir görseldir ve hiçbir değer okumaz.

**GV20.** Bu ölçüm yaprak bileşenleri okudukları değerler üzerinden ele alır. Bileşenlerin çizim maliyeti, simge kümesinin boyutu ve varlık kataloğunun çözümlenmesi ölçülmedi.

#### Renk Şeması ve Dolgu Oranı

Aşağıdaki ağaçta renk şeması ortamdan gelir ve simgeyle avatar tarafından okunur. Dolgu oranı paneli kuran ekranın, `MeterScreen`'in durumudur; panel onu `let` parametresi olarak alır ve şekle geçirir. Avatarın yükleniyor durumu ise avatarın kendisine aittir.

```swift
// main.swift — renk semasi ortam, dolgu orani ebeveyn, yukleme durumu yerel
let storage: [String: Storage] = [
    "colorScheme": .environment,
    "fillRatio": .state(owner: "MeterScreen"),
    "isLoading": .state(owner: "AvatarImage"),
]

let screen = ViewTree(nodes: [
    ViewNode("MeterScreen", parent: nil, reads: ["fillRatio"]),
    ViewNode("MeterPanel", parent: "MeterScreen", reads: ["fillRatio"]),
    ViewNode("MeterShape", parent: "MeterPanel", reads: ["fillRatio"]),
    ViewNode("MeterIcon", parent: "MeterPanel", reads: ["colorScheme"]),
    ViewNode("AvatarImage", parent: "MeterPanel", reads: ["isLoading", "colorScheme"]),
], storage: storage)

let recomputes = [
    screen.change("renk-semasi-degisti", key: "colorScheme"),
    screen.change("doluluk-degisti", key: "fillRatio"),
    screen.change("yukleme-bitti", key: "isLoading"),
].flatMap { $0 }

countCauses(recomputes)
```

```
renk-semasi-degisti: colorScheme degisti | yeniden MeterIcon ortam, AvatarImage ortam | atlanan MeterScreen,MeterPanel,MeterShape
doluluk-degisti: fillRatio degisti | yeniden MeterScreen yerel, MeterPanel ebeveyn, MeterShape ebeveyn | atlanan MeterIcon,AvatarImage
yukleme-bitti: isLoading degisti | yeniden AvatarImage yerel | atlanan MeterScreen,MeterPanel,MeterShape,MeterIcon
yerel: 2
ebeveyn: 2
gozlem: 0
ortam: 2
```

`renk-semasi-degisti` satırında simge ve avatar ortam tetiğiyle yeniden hesaplandı; ekran, panel ve şekil atlandı. `doluluk-degisti` satırında değerin sahibi olan ekran yerel, panel ile şekil ebeveyn tetiğiyle yeniden hesaplandı. `yukleme-bitti` satırında yalnız avatar yerel tetikle yeniden hesaplandı: yükleniyor durumunu avatar kendi bünyesinde tutar.

Kaynakta karşılığı, uzak bir görseli yükleyen bir görünümdür. Yükleme sürerken bir yer tutucu, bittiğinde görselin kendisi gösterilir; durum değişince yalnız bu görünüm yeniden hesaplanır.

```swift
// Kaynak parca: uzak gorsel yuklenirken yer tutucu gosterir
import SwiftUI

struct AvatarImage: View {
    let url: URL?

    var body: some View {
        AsyncImage(url: url) { image in
            image.resizable()
        } placeholder: {
            ProgressView()
        }
    }
}
```

**GV21.** Bu ölçüm `colorScheme`, `fillRatio` ve `isLoading` için. Yükleme durumunun gerçekten görünümün kendi durumu olup olmadığı, yani çerçevenin bu durumu nerede sakladığı ölçülmedi; modelde avatar bunun sahibi sayıldı. Dolgu oranının sahibi olarak eklenen ekran da bir model varsayımıdır; kaynak parça yalnız panelin parametresini gösterir.

#### Sınırlayıcı Ölçüm

Statik bir görsel hiçbir değer okumaz. Bu yüzden hiçbir değişiklik onu yeniden hesaplatmaz. Aşağıdaki ağaçta logo görseli okuma kümesi boş bir yapraktır; panel kendi modunu, etiket ise panelin modunu okur.

```swift
// main.swift — statik gorsel hicbir deger okumaz
let storage: [String: Storage] = [
    "mode": .state(owner: "LogoPanel"),
]

let screen = ViewTree(nodes: [
    ViewNode("LogoPanel", parent: nil, reads: ["mode"]),
    ViewNode("StaticLogo", parent: "LogoPanel", reads: []),
    ViewNode("LogoCaption", parent: "LogoPanel", reads: ["mode"]),
], storage: storage)

let recomputes = [
    screen.change("mod-degisti", key: "mode"),
    screen.change("varlik-degisti", key: "logoAsset"),
].flatMap { $0 }

countCauses(recomputes)
```

```
mod-degisti: mode degisti | yeniden LogoPanel yerel, LogoCaption ebeveyn | atlanan StaticLogo
varlik-degisti: logoAsset degisti | yeniden yok | atlanan LogoPanel,StaticLogo,LogoCaption
yerel: 1
ebeveyn: 1
gozlem: 0
ortam: 0
```

`mod-degisti` satırında panel yerel, etiket ebeveyn tetiğiyle yeniden hesaplandı; logo atlandı. `varlik-degisti` satırında anahtarı okuyan görünüm olmadığı için hiçbir görünüm yeniden hesaplanmadı: üç görünüm de atlandı. Statik görsel, ağaçta bulunur ama hiçbir tetiğe duyarlı değildir; yeniden hesaplanması için bir değer okuması gerekir. Ebeveyninin onu yeniden kurmasının etkisi GV22'de ölçülmedi.

**GV22.** Bu ölçüm ebeveynin yeniden hesaplanmasının statik çocuğu yeniden kurmadığı varsayımına dayanır. Çerçevenin ebeveyn yeniden hesaplanınca çocuk görünümlerin gövdesini yeniden çalıştırıp çalıştırmadığı ölçülmedi.

#### Özet

- Metin, simge, şekil ve görsel ağacın yapraklarıdır; her biri biçimini bir değerden alır.
- Renk şeması ortamdan okunur; okuyan simge ve avatar ortam tetiğiyle yeniden hesaplanır.
- Şeklin dolgu oranını veren ebeveyn durumu, şekil için ebeveyn tetiği üretir.
- Uzak görselin yükleniyor durumu görselin kendisine aittir ve yerel tetik üretir.
- Statik görsel hiçbir değer okumaz; hiçbir değişiklikte yeniden hesaplanmaz.
- Bu dersin tetiği: **ortam**.

#### Sonraki Adım

Bu derste yapraklar değerlerini ortamdan, ebeveynden ve kendi bünyesinden aldı. Sıradaki derste ağacı kuran araca bakıyoruz: görünüm kurucu. Bir koşula bağlı içerik ya da bir kapanışla kurulan alt görünüm, hangi değere bağlıdır. Tetik bu kez ebeveyndir.

### Görünüm Kurucular

Source: https://academia.sh/tr/kurslar/swiftui/gorunum-kurucular

Kurucu içeriğindeki koşulun ebeveyn durumuna bağlı olduğunda ebeveyn tetiği ürettiğinin, kurucu bileşenin kendi basılı durumunun yerel kaldığının ve kapanışın ağaçta düğüm olmadığının ölçümü.

Önceki derste yapraklar değerlerini ortamdan, ebeveynden ve kendi bünyesinden aldı. Bu derste ağacı kuran araca bakıyoruz: görünüm kurucu. Bir gövdede alt alta yazılan ifadeler, bir koşul ya da bir döngü, kurucu tarafından tek bir görünüme toplanır. Kurucu bir bileşenin içeriği de bir kapanış olarak verilebilir. Soru aynı kalır: **bu yeniden hesaplamayı ne tetikledi?** Bu derste bakılan tetik, **ebeveyn**.

```swift
// 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, açılış dersindekiyle aynıdır. Bu dersin ağacı bir açılır paneldir: paneli kuran ekran, panel, ayrıntı bloğu, basılabilir düğme ve etiketi.

#### İfadelerden Görünüme

Görünüm kurucu, gövdedeki ifadeleri tek bir değerde toplayan bir yapıdır. İki metin alt alta yazıldığında ikisi birlikte tek bir görünüm olur; bir koşul yazıldığında doğru ve yanlış dalın yalnız biri gösterilir. Aşağıdaki parça panelin ayrıntı bloğunu bir koşula bağlar.

```swift
// Kaynak parca: kurucu koşulu ve ifadeleri tek gorunume toplar
import SwiftUI

struct DisclosurePanel: View {
    let showDetails: Bool
    let itemCount: Int

    var body: some View {
        VStack {
            Text("Oge sayisi: \(itemCount)")
            if showDetails {
                Text("Ayrintilar burada")
            }
        }
    }
}
```

`showDetails` panelin sahibinden gelen bir parametredir ve koşulun doğru ya da yanlış olmasını belirler. Koşul yanlışken ayrıntı bloğu ağaçta yoktur; doğruyken vardır. Modelde bu, ayrıntı bloğunun `showDetails` değerini okuyan bir düğüm olmasıdır.

**GV23.** Bu ölçüm koşullu bloğu her zaman ağaçta bulunan, ama `showDetails` değerini okuyan bir düğüm olarak modeller. Koşul yanlışken bloğun ağaçtan çıkması, yani kimliğin yok olması ve yeniden eklenmesi ölçülmedi.

#### Koşul ve Kurucu Bileşen

Aşağıdaki ağaçta `showDetails` ve `itemCount` değerlerinin sahibi paneli kuran `PanelScreen`'dir; panel ikisini `let` parametresi olarak alır, ayrıntı bloğu ilkini okur. Basılabilir düğme kendi `isPressed` durumunun sahibidir ve etiketi bu değeri okuyarak vurgu verir.

```swift
// main.swift — kosul ebeveyn, kurucu bilesenin basili durumu yerel
let storage: [String: Storage] = [
    "showDetails": .state(owner: "PanelScreen"),
    "itemCount": .state(owner: "PanelScreen"),
    "isPressed": .state(owner: "PressableButton"),
]

let screen = ViewTree(nodes: [
    ViewNode("PanelScreen", parent: nil, reads: ["showDetails", "itemCount"]),
    ViewNode("DisclosurePanel", parent: "PanelScreen", reads: ["showDetails", "itemCount"]),
    ViewNode("DetailBlock", parent: "DisclosurePanel", reads: ["showDetails"]),
    ViewNode("PressableButton", parent: "DisclosurePanel", reads: ["isPressed"]),
    ViewNode("ButtonLabel", parent: "PressableButton", reads: ["isPressed"]),
], storage: storage)

let recomputes = [
    screen.change("detay-acildi", key: "showDetails"),
    screen.change("sayi-degisti", key: "itemCount"),
    screen.change("basildi", key: "isPressed"),
].flatMap { $0 }

countCauses(recomputes)
```

```
detay-acildi: showDetails degisti | yeniden PanelScreen yerel, DisclosurePanel ebeveyn, DetailBlock ebeveyn | atlanan PressableButton,ButtonLabel
sayi-degisti: itemCount degisti | yeniden PanelScreen yerel, DisclosurePanel ebeveyn | atlanan DetailBlock,PressableButton,ButtonLabel
basildi: isPressed degisti | yeniden PressableButton yerel, ButtonLabel ebeveyn | atlanan PanelScreen,DisclosurePanel,DetailBlock
yerel: 3
ebeveyn: 4
gozlem: 0
ortam: 0
```

`detay-acildi` satırında değerin sahibi olan ekran yerel, panel ile ayrıntı bloğu ebeveyn tetiğiyle yeniden hesaplandı; düğme ve etiketi atlandı. `sayi-degisti` satırında ekran yerel, panel ebeveyn tetiğiyle yeniden hesaplandı; ayrıntı bloğu sayıyı okumadığından atlandı. `basildi` satırında düğme yerel, etiket ebeveyn tetiğiyle yeniden hesaplandı.

Kurucu içeriğindeki koşul ebeveynin durumuna bağlıysa, koşulu kullanan blok ebeveyn tetiğiyle yeniden hesaplanır. Düğmenin kendi basılı durumu ise düğmenin dışına taşmaz: panel ve ayrıntı bloğu bu değişiklikte atlanır.

**GV24.** Bu ölçüm `showDetails`, `itemCount` ve `isPressed` için. Düğmenin basılı durumunun hangi hareket olayıyla değiştiği, yani dokunma ile kaldırma arasındaki geçiş ölçülmedi. Değerlerin sahibi olarak eklenen ekran bir model varsayımıdır; kaynak parça yalnız panelin parametrelerini gösterir.

#### Kurucu Bileşen

Bir bileşenin içeriğini kapanış olarak almak, kurucu bileşenin tipik biçimidir. Bileşen kapanışı çağırır ve ortaya çıkan görünümü kendi düzeninin içine koyar. Aşağıdaki parça bir kart bileşenini kurucuyla tanımlar.

```swift
// Kaynak parca: icerigi kapanis olarak alan kurucu bilesen
import SwiftUI

struct InfoCard<Content: View>: View {
    @State private var isPressed = false
    let content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        content
            .padding()
            .opacity(isPressed ? 0.6 : 1)
            .onTapGesture { isPressed.toggle() }
    }
}
```

Bileşen, kendi basılı durumunun sahibidir ve içeriğin kendisine dokunmaz. Kullanan taraf kartın içeriğini kapanışla verir; kapanış çağrılır ve sonuç bir görünüm değeri olarak saklanır.

#### Sınırlayıcı Ölçüm

Kapanışın kendisi bir görünüm değildir. Bir değeri çağrıldığında üretir ve çağrıldığı yerde biter. Bu yüzden ağaçta bir düğüm olmaz; kapanışın içinde okunan değer, kapanışı çağıran görünüme değil, içeriği kuran görünüme aittir. Aşağıdaki ağaçta ekran, kartın içeriğini kurar; kart yalnız kabuktur.

```swift
// main.swift — kapanis agacta dugum degildir, okuma icerigi kuran gorunume aittir
let storage: [String: Storage] = [
    "title": .state(owner: "HostScreen"),
]

let screen = ViewTree(nodes: [
    ViewNode("HostScreen", parent: nil, reads: ["title"]),
    ViewNode("CardShell", parent: "HostScreen", reads: []),
    ViewNode("CardContent", parent: "CardShell", reads: ["title"]),
], storage: storage)

let recomputes = [
    screen.change("baslik-degisti", key: "title"),
    screen.change("kapanis-degisti", key: "contentClosure"),
].flatMap { $0 }

countCauses(recomputes)
```

```
baslik-degisti: title degisti | yeniden HostScreen yerel, CardContent ebeveyn | atlanan CardShell
kapanis-degisti: contentClosure degisti | yeniden yok | atlanan HostScreen,CardShell,CardContent
yerel: 1
ebeveyn: 1
gozlem: 0
ortam: 0
```

`baslik-degisti` satırında ekran yerel, içerik ebeveyn tetiğiyle yeniden hesaplandı; kart atlandı. Başlığı okuyan içeriktir, kabuk değildir. `kapanis-degisti` satırında hiçbir görünüm yeniden hesaplanmadı: kapanışın kendisi saklama tablosunda ve okuma kümelerinde yoktur, çünkü ağaçta düğüm olmaz.

**GV25.** Bu ölçüm kapanışı ağaçta düğüm olmayan bir yapı olarak ele alır ve onu bir anahtar olarak bile modellemez. Kapanışın hangi görünümün gövdesinde değerlendirildiği ve kurucunun içeriği ne zaman çağırdığı ölçülmedi.

#### Özet

- Görünüm kurucu, gövdedeki ifadeleri, koşulları ve döngüleri tek bir görünüme toplar.
- Kurucu içeriğindeki koşul ebeveynin durumuna bağlıysa, koşulu kullanan blok ebeveyn tetiğiyle yeniden hesaplanır.
- Kurucu bileşenin kendi basılı durumu yerel tetik üretir ve bileşenin dışına taşmaz.
- İçerik kapanışı bir görünüm değildir ve ağaçta düğüm olmaz.
- Kapanışın içinde okunan değer, içeriği kuran görünüme aittir; kabuk onu okumaz.
- Bu dersin tetiği: **ebeveyn**.

#### Sonraki Adım

Bu konuyu bitirdik: görünümler ağaçta nasıl kurulur ve değerler ağaçta nasıl akar. Sıradaki konu, bir görünümün kendi durumunu ele alır; ilk dersi Yerel Durum'dur: bir görünümün kendi sayacını saklaması neyi değiştirir, kim yeniden hesaplanır. Tetik yerel olacaktır.

## Durum ve Gezinme

Görünüme ait yerel durum, durumun alt görünümle bağlama üzerinden paylaşımı, görünüm dışı gözlemlenebilir nesneler, ağaç boyunca aktarılan ortam değerleri, programlı gezinme yığıtı ve yolu, sayfa, uyarı ve tabaka sunumu, sekme ve araç çubuğu.

### Yerel Durum

Source: https://academia.sh/tr/kurslar/swiftui/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ü.

Ö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.

```swift
// 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.

```swift
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.

```swift
// 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.

```swift
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.

```swift
// 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.

```swift
// 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-disi` kalı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.

### Bağlama

Source: https://academia.sh/tr/kurslar/swiftui/baglama

Bir bağlamayla ebeveynin durumunu yazan çocuğun ebeveyn tetiğiyle yeniden hesaplandığının ve tetiğin değişikliği kimin başlattığına değil değerin sahibine bağlı olduğunun ölçümü.

Önceki derste durumu ataya kaldırdık ve çocuğa iki şey verdik: değerin kendisi ve değeri değiştirmesi için bir kapanış. Bu iki parçanın birleşimi o kadar sık gerekir ki çerçevede tek bir kavrama dönüşmüştür: **bağlama** (binding). Bağlama, bir değerin kendisi değil, değerin sahibine açılan okuma ve yazma yoludur. Bu derste bakılan tetik **ebeveyn**: çocuk görünüm, sahibi başka olan bir değeri okuyor ve yazıyor.

Ölçüm aynı çekirdek dosyayla yapılır.

```swift
// 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)")
    }
}
```

Çekirdekte bağlamanın ayrı bir tipi yoktur. `Storage.state(owner:)` değerin sahibini söyler, `reads` hangi görünümün onu okuduğunu söyler. Bir bağlama üzerinden okuyan görünüm de okuyan görünümdür; model, çerçevenin bağımlılık izleme kuralını kurar, çerçevenin kendisi değildir.

#### Sahibine Açılan Bağ

Bir ekranın ses düzeyini tuttuğunu düşünelim. Düzey ekranın durumudur; ekran onu bir adım düğmesine verir ve düğme düzeyi artırabilmelidir. Çocuk durumu sahiplenmez, bir bağlama alır. Ebeveyn bağlamayı değerin önüne `$` koyarak kurar; çocuk onu `@Binding` ile bildirir.

```swift
import SwiftUI

struct VolumeScreen: View {
    @State private var volume = 3

    var body: some View {
        VStack {
            Text("Ses: \(volume)")
            VolumeStepper(volume: $volume)
            FooterNote()
        }
    }
}

struct VolumeStepper: View {
    @Binding var volume: Int
    @State private var isPressed = false

    var body: some View {
        Button(isPressed ? "Basili" : "Yukselt \(volume)") {
            volume += 1
            isPressed = true
        }
    }
}
```

`VolumeStepper` iki farklı değere sahip. `volume` bir bağlamadır: değer ekrana aittir, çocuk onu yalnız okur ve yazar. `isPressed` ise çocuğun kendi durumudur: yalnız düğmeyi ilgilendirir, ekranın başka yerinde kimse bilmez. Önceki dersin ayrımı burada yan yana durur; biri ata–torun yolunda akan, öteki sahibinde kalan değerdir.

Ağaçta `VolumeScreen` ve `VolumeStepper` iki değeri paylaşır. `volume` ekranındır; ekranın gövdesi onu gösterir, düğmenin gövdesi de gösterir. `FooterNote` ise hiçbirini okumaz. İki değişiklik verilir: biri bağlamanın değeri, biri çocuğun geçici durumu.

```swift
// main.swift — baglama ebeveyn tetigi, cocugun kendi durumu yerel tetik uretir
let storage: [String: Storage] = [
    "volume": .state(owner: "VolumeScreen"),
    "isPressed": .state(owner: "VolumeStepper"),
]

let screen = ViewTree(nodes: [
    ViewNode("VolumeScreen", parent: nil, reads: ["volume"]),
    ViewNode("VolumeStepper", parent: "VolumeScreen", reads: ["volume", "isPressed"]),
    ViewNode("FooterNote", parent: "VolumeScreen", reads: []),
], storage: storage)

let recomputes = [
    screen.change("duzey-yukseldi", key: "volume"),
    screen.change("dugme-basildi", key: "isPressed"),
].flatMap { $0 }

countCauses(recomputes)
```

```
duzey-yukseldi: volume degisti | yeniden VolumeScreen yerel, VolumeStepper ebeveyn | atlanan FooterNote
dugme-basildi: isPressed degisti | yeniden VolumeStepper yerel | atlanan VolumeScreen,FooterNote
yerel: 2
ebeveyn: 1
gozlem: 0
ortam: 0
```

`duzey-yukseldi` satırında iki görünüm yeniden hesaplandı. `VolumeScreen` değerin sahibi, yerel; `VolumeStepper` değeri bir bağlamayla okuyan alt görünüm, ebeveyn. Düzeyi yazan çocuktu, ama çocuk sahibi değildi. `dugme-basildi` satırında yalnız `VolumeStepper` yeniden hesaplandı ve yerel: `isPressed` çocuğa aittir, bağlamayla ekrana bağlı değildir ve ekran bu değişiklikten haberdar olmaz. `FooterNote` iki değişiklikte de atlandı.

Sayılar şöyle çıkar: iki değişiklik üç yeniden hesaplama üretti; ikisi yerel, biri ebeveyn. Aynı çocuk görünüm iki satırda iki farklı tetikle göründü: bir değer bağlamayla geldiği için ebeveyn, bir değer kendisinin olduğu için yerel. Görünümün türü tetiği belirlemiyor, her okunan değerin sahibi belirliyor.

**DR4.** Bu ölçüm iki değişiklik ve iki anahtar için. Düğmenin eylemindeki iki atama tek bir değişiklik olarak ele alınmadı; `volume` ve `isPressed` ayrı ölçüldü. Çerçevenin iki değişikliği aynı döngüde birleştirip birleştirmediği ölçülmedi.

#### Bağlama ile Parametre Aynı Tetiği Üretir

Çocuğa değeri bağlama yerine düz bir parametre olarak vermek de mümkündür. Parametreyi alan görünüm değeri okur ama yazamaz; yazmak için bir kapanışa ihtiyaç duyar, önceki dersteki düzen budur. Soru, tetiğin bu iki biçimde farklı olup olmadığıdır.

```swift
import SwiftUI

struct VolumeScreen: View {
    @State private var volume = 3

    var body: some View {
        VStack {
            VolumeStepper(volume: $volume)
            VolumeBadge(level: volume)
        }
    }
}

struct VolumeBadge: View {
    let level: Int

    var body: some View {
        Text("Duzey \(level)")
    }
}
```

`VolumeStepper` değeri yazabilen çocuktur, `VolumeBadge` yalnız gösteren. İkisi de aynı değeri okur. Ağaç iki çocuğu bir arada kurar ve tek bir değişiklik uygular.

```swift
// main.swift — okuyan cocuk baglamayla da parametreyle de ayni tetigi alir
let storage: [String: Storage] = [
    "volume": .state(owner: "VolumeScreen"),
]

let screen = ViewTree(nodes: [
    ViewNode("VolumeScreen", parent: nil, reads: ["volume"]),
    ViewNode("VolumeStepper", parent: "VolumeScreen", reads: ["volume"]),
    ViewNode("VolumeBadge", parent: "VolumeScreen", reads: ["volume"]),
], storage: storage)

let recomputes = [
    screen.change("duzey-yukseldi", key: "volume"),
].flatMap { $0 }

countCauses(recomputes)
```

```
duzey-yukseldi: volume degisti | yeniden VolumeScreen yerel, VolumeStepper ebeveyn, VolumeBadge ebeveyn | atlanan yok
yerel: 1
ebeveyn: 2
gozlem: 0
ortam: 0
```

Üç görünüm yeniden hesaplandı: ekran yerel, iki çocuk ebeveyn. Bağlamayla okuyan `VolumeStepper` ile parametreyle okuyan `VolumeBadge` arasında çıktıda fark yok. Bu bir bulgudur ve bir sınırdır. Bulgu şudur: yeniden hesaplamayı okuma tetikler, yazma yetkisi değil. Sınır şudur: model yazma yetkisini ayırmaz, `ViewNode` yalnız neyin okunduğunu söyler. Bağlamanın çocuğa yazma yolu vermesi kod düzeyinde bir fark; tetik düzeyinde değildir.

**DR5.** Bu ölçüm iki çocuğun ikisi de değeri gövdede okuduğu durum için. Bir çocuğun bağlamayı yalnız yazıp hiç okumadığı durum ölçülmedi; o durumda `reads` içine girmeyeceği için model onu atlanan olarak sınıflardı.

#### Sınırlayıcı Ölçüm

Bir değişikliği kimin başlattığı ile kimin tetiklendiği ayrı sorulardır. Çocuk bağlama üzerinden yazdığında akla gelen ilk okuma, tetiğin çocuğa ait olduğudur. Kural bunu söylemez: **kural yazanı okumaz**. Tetik, değerin sahibine ve okuyana bakar; değişikliği kimin yazdığına bakmaz. Aşağıdaki ölçüm bunu aynı anahtarın iki değişikliğiyle gösterir: biri çocuğun yazmasını, biri ebeveynin yazmasını temsil eder. Modelde ikisinin farkı yalnız etikettedir.

```swift
// main.swift — kural yazani okumaz, tetik degerin sahibine gore belirlenir
let storage: [String: Storage] = [
    "volume": .state(owner: "VolumeScreen"),
]

let screen = ViewTree(nodes: [
    ViewNode("VolumeScreen", parent: nil, reads: ["volume"]),
    ViewNode("VolumeStepper", parent: "VolumeScreen", reads: ["volume"]),
], storage: storage)

let recomputes = [
    screen.change("cocuk-yazdi", key: "volume"),
    screen.change("ebeveyn-yazdi", key: "volume"),
].flatMap { $0 }

countCauses(recomputes)
```

```
cocuk-yazdi: volume degisti | yeniden VolumeScreen yerel, VolumeStepper ebeveyn | atlanan yok
ebeveyn-yazdi: volume degisti | yeniden VolumeScreen yerel, VolumeStepper ebeveyn | atlanan yok
yerel: 2
ebeveyn: 2
gozlem: 0
ortam: 0
```

İki satır birebir aynı: ekran yerel, çocuk ebeveyn. Yazanın çocuk olması çocuğu yerel yapmadı. Sayımda yerel 2, ebeveyn 2 çıktı; yazan görünüm hiçbir sayıyı değiştirmedi. Bu dersin tetiği bu yüzden değişikliği çocuğun başlatmasıyla tanımlanmaz, çocuğun sahibi olmadığı bir değeri okumasıyla tanımlanır.

**DR6.** Bu ölçüm model düzeyinde bir eşitliği gösterir: aynı anahtar, aynı ağaç, aynı sonuç. Çerçevenin çocuktan gelen yazmayı ebeveynden gelen yazmadan farklı sıraladığı ya da gruplandığı ölçülmedi.

#### Özet

- Bağlama, bir değerin kopyası değil, sahibine açılan okuma ve yazma yoludur.
- Bağlamayla ebeveyn durumunu okuyan çocuk, değer değişince ebeveyn tetiğiyle yeniden hesaplanır; durumun sahibi yerel tetikle yeniden hesaplanır.
- Çocuğun kendi geçici durumu bağlamadan bağımsızdır ve yalnız çocuğu yeniden hesaplatır, tetik yereldir.
- Bağlamayla okuyan ile parametreyle okuyan çocuk aynı tetiği alır; model yazma yetkisini ayırmaz.
- Kural yazanı okumaz: aynı değişiklik çocuktan da ebeveynden de gelse tetik değerin sahibine göre belirlenir.
- Bu dersin tetiği: **ebeveyn**.

#### Sonraki Adım

Şimdiye kadar değerlerin hepsi bir görünümde yaşadı ve ya o görünümde ya da onun altında okundu. Sıradaki derste değer bir görünümde değil, ayrı bir nesnede yaşar ve onu okuyan görünümler ağacın herhangi bir yerinde olabilir. Soru aynı kalır, ama tetik yeni bir adla anılır: gözlem.

### Gözlemlenebilir Nesneler

Source: https://academia.sh/tr/kurslar/swiftui/gozlemlenebilir-nesneler

Gözlemlenebilir bir nesnenin yalnız okunan özelliğinin değişimde yeniden hesaplama ürettiğinin, gerçek gözlem izlemesiyle doğrulanmasının ve nesne düzeyinde bildirimin neyi değiştirdiğinin ölçümü.

Önceki derste çocuk görünüm bir değeri bağlamayla okudu; değer yine bir görünümde yaşıyordu. Bu derste değer bir görünümün içinde değil, ayrı bir nesnede yaşar. Ekranın verisi, ağın ya da diskin sonucu, birden çok ekranın paylaştığı bir liste: bunlar bir görünümün alanı olmak yerine bir **gözlemlenebilir nesne** (observable object) içinde durur. Görünümler nesneyi tutar, özelliklerini okur ve okudukları özellik değişince yeniden hesaplanır. Bu derste bakılan tetik **gözlem**.

Ölçüm çekirdek dosyayla yapılır ve bu derste ilk kez çekirdeğin dışında, derleyicinin kendi gözlem izlemesi de ölçülür.

```swift
// 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.model(object:)` bir değerin bir görünümde değil, bir nesnede saklandığını söyler. `cause` işlevi bu durumda sahiplik sorusuna girmez: bir nesne özelliğini okuyan her görünüm, ağaçta nerede durursa dursun, gözlem tetiğini alır. Model, çerçevenin bağımlılık izleme kuralını kurar; çerçevenin kendisi değildir.

#### Özellik Düzeyinde Gözlem

Aşağıdaki nesne bir kütüphanenin kitap listesini ve ödünç sayısını tutar. `@Observable` makrosu sınıfın her saklanan özelliğini izlenebilir yapar. Ekran nesneyi çocuklarına verir; her çocuk yalnız ilgilendiği özelliği okur.

```swift
import SwiftUI
import Observation

@Observable
final class LibraryStore {
    var books: [String] = []
    var borrowedCount = 0
}

struct LibraryScreen: View {
    let store: LibraryStore
    @State private var searchText = ""

    var body: some View {
        VStack {
            BookList(store: store, query: searchText)
            BorrowBadge(store: store)
        }
    }
}

struct BookList: View {
    let store: LibraryStore
    let query: String

    var body: some View {
        List(store.books.filter { $0.contains(query) }, id: \.self) { title in
            Text(title)
        }
    }
}

struct BorrowBadge: View {
    let store: LibraryStore

    var body: some View {
        Text("Odunc: \(store.borrowedCount)")
    }
}
```

`BookList` yalnız `books` özelliğini, `BorrowBadge` yalnız `borrowedCount` özelliğini okur. `searchText` ise ekranın kendi durumudur ve nesnede değildir. Ağaç bu okumaları birebir taşır. Saklama tablosunda iki nesne özelliği `LibraryStore` nesnesine, arama metni ise `LibraryScreen` görünümüne bağlanır. Üç değişiklik verilir: bir kitap eklenir, bir ödünç sayılır, bir arama yazılır.

```swift
// main.swift — nesne ozelligi gozlem, ekranin kendi durumu yerel tetik uretir
let storage: [String: Storage] = [
    "books": .model(object: "LibraryStore"),
    "borrowedCount": .model(object: "LibraryStore"),
    "searchText": .state(owner: "LibraryScreen"),
]

let screen = ViewTree(nodes: [
    ViewNode("LibraryScreen", parent: nil, reads: ["searchText"]),
    ViewNode("BookList", parent: "LibraryScreen", reads: ["books", "searchText"]),
    ViewNode("BorrowBadge", parent: "LibraryScreen", reads: ["borrowedCount"]),
], storage: storage)

let recomputes = [
    screen.change("kitap-eklendi", key: "books"),
    screen.change("odunc-verildi", key: "borrowedCount"),
    screen.change("arama-yazildi", key: "searchText"),
].flatMap { $0 }

countCauses(recomputes)
```

```
kitap-eklendi: books degisti | yeniden BookList gozlem | atlanan LibraryScreen,BorrowBadge
odunc-verildi: borrowedCount degisti | yeniden BorrowBadge gozlem | atlanan LibraryScreen,BookList
arama-yazildi: searchText degisti | yeniden LibraryScreen yerel, BookList ebeveyn | atlanan BorrowBadge
yerel: 1
ebeveyn: 1
gozlem: 2
ortam: 0
```

`kitap-eklendi` yalnız `BookList`'i, `odunc-verildi` yalnız `BorrowBadge`'i yeniden hesaplattı; ikisi de gözlem. İki görünüm aynı nesneyi tutuyor, ama her biri nesnenin farklı bir özelliğini okuyor ve birinin değişimi ötekini etkilemiyor. `arama-yazildi` satırı gözlem değil: değer nesnede değil ekranda yaşıyor, bu yüzden `LibraryScreen` yerel, `BookList` ebeveyn tetikle yeniden hesaplandı. Sayılar: yerel bir, ebeveyn bir, gözlem iki.

Burada tetiği belirleyen nesnenin kendisi değil, okunan özelliktir. Aynı `BookList` hem `books` için gözlem hem `searchText` için ebeveyn tetikle yeniden hesaplandı. Bu, ikinci dersin sonucunun nesneler için de geçerli olduğunu gösterir: tetik görünümün türüne değil, okunan değerin yaşadığı yere bağlıdır.

**DR7.** Bu ölçüm üç değişiklik ve üç anahtar için. Nesne özelliğinin görünümün gövdesinde yalnız dolaylı okunduğu durum, örneğin bir hesaplanmış özellik üzerinden okuma, ölçülmedi; modelde her görünüm okuduğu anahtarı doğrudan `reads` içinde taşır.

#### Gerçek İzleme: Yalnız Okunan Özellik

Yukarıdaki ölçüm bir modeldir; ağaç kuralı bildiğimiz biçimde kurar. Kuralın kendisi, yani `@Observable` bir nesnede yalnız okunan özelliğin değişiminin bildirim üretmesi, derleyicinin ve Observation modülünün davranışıdır ve doğrudan ölçülebilir. `withObservationTracking`, ilk kapanışta okunan özellikleri izler ve bunlardan biri değişince ikinci kapanışı çağırır.

```swift
// main.swift — gercek gozlem izleme: yalniz okunan ozellik bildirim uretir
import Observation

@Observable
final class LibraryStore {
    var books: [String] = []
    var borrowedCount = 0
}

let library = LibraryStore()
withObservationTracking {
    _ = library.borrowedCount
} onChange: {
    print("izlenen ozellik degisti")
}
library.books.append("algoritmalar")
print("books degisti")
library.borrowedCount += 1
print("borrowedCount degisti 1")
library.borrowedCount += 1
print("borrowedCount degisti 2")
```

```
books degisti
izlenen ozellik degisti
borrowedCount degisti 1
borrowedCount degisti 2
```

İzleme yalnız `borrowedCount` özelliğini okudu. `books` değiştiğinde geri çağrı çalışmadı: çıktıda `books degisti` satırından önce bir bildirim yok. `borrowedCount` değiştiğinde geri çağrı çalıştı ve `izlenen ozellik degisti` satırı, atamanın hemen ardından, `borrowedCount degisti 1` satırından önce basıldı. Yani bildirim atamanın içinde ve eşzamanlı geldi. İkinci artışta ise bildirim gelmedi: izleme **bir kezliktir**. Bir değişiklik bildirildikten sonra izleme biter ve sonraki değişikliği görmek için yeniden kurulması gerekir. Görünümlerde bu yeniden kurma çerçevenin işidir; bu ders onu ölçmez.

Bu iki gözlem, ağaçtaki iki satırı destekler. Okunmayan özelliğin değişimi hiçbir şeyi yeniden hesaplatmaz, okunan özelliğin değişimi yalnız o okumayı kuran tarafı bilgilendirir. Ağaçta `BorrowBadge`'in `books` değişiminde atlanması bu davranışın modele yansımasıdır.

**DR8.** Bu ölçüm `Observation` modülünün kendisi içindir; çerçevenin bu izlemeyi görünüm gövdesine nasıl bağladığı ve gövdenin kaç kez çağrıldığı ölçülmedi. Geri çağrının atanan değer yazılmadan önce mi sonra mı çalıştığı da ölçülmedi; çıktı yalnız atamanın içinde çalıştığını gösterir.

#### Sınırlayıcı Ölçüm

Her gözlem mekanizması özellik düzeyinde değildir. Daha eski bir yol, `ObservableObject` ve `@Published`, nesne düzeyinde bildirim yapar: nesnenin herhangi bir yayımlanan özelliği değişince nesneyi izleyen her görünüm yeniden hesaplanır. Bu yol Combine çerçevesine bağlıdır; ölçüm ortamında derlenmediği için kaynağı yalnız gösterilebilir.

```swift
import SwiftUI
import Combine

final class LibraryStore: ObservableObject {
    @Published var books: [String] = []
    @Published var borrowedCount = 0
}

struct BookList: View {
    @ObservedObject var store: LibraryStore

    var body: some View {
        List(store.books, id: \.self) { title in
            Text(title)
        }
    }
}

struct BorrowBadge: View {
    @ObservedObject var store: LibraryStore

    var body: some View {
        Text("Odunc: \(store.borrowedCount)")
    }
}
```

Aşağıdaki ölçüm bu kuralın modelini kurar; çerçevenin kendisini ölçmez. Nesne düzeyindeki bildirimde saklama tablosunda tek bir anahtar vardır ve anahtar nesnenin adıdır. İzleyen her görünüm bu anahtarı okur, çünkü nesnedeki hangi özelliğin değiştiği görünüm tarafından ayırt edilmez.

```swift
// main.swift — nesne duzeyinde bildirimde anahtar nesnenin adidir
let storage: [String: Storage] = [
    "LibraryStore": .model(object: "LibraryStore"),
]

let screen = ViewTree(nodes: [
    ViewNode("LibraryScreen", parent: nil, reads: []),
    ViewNode("BookList", parent: "LibraryScreen", reads: ["LibraryStore"]),
    ViewNode("BorrowBadge", parent: "LibraryScreen", reads: ["LibraryStore"]),
], storage: storage)

let recomputes = [
    screen.change("kitap-eklendi", key: "LibraryStore"),
    screen.change("odunc-verildi", key: "LibraryStore"),
].flatMap { $0 }

countCauses(recomputes)
```

```
kitap-eklendi: LibraryStore degisti | yeniden BookList gozlem, BorrowBadge gozlem | atlanan LibraryScreen
odunc-verildi: LibraryStore degisti | yeniden BookList gozlem, BorrowBadge gozlem | atlanan LibraryScreen
yerel: 0
ebeveyn: 0
gozlem: 4
ortam: 0
```

İki değişiklik iki görünümü ikişer kez yeniden hesaplattı; gözlem sayısı dört. Özellik düzeyindeki ilk ölçümde aynı iki değişiklik iki yeniden hesaplama üretmişti. Fark, kuralın ayrıntısından gelir: nesne düzeyinde bildirimde anahtar nesnenin adı olduğu için, `kitap-eklendi` değişikliği `BorrowBadge`'i de yeniden hesaplattı. Bu satırlar çerçevenin ölçümü değildir, kuralın modelidir. Ders bu modeli, özellik düzeyindeki gözlemin neyi kazandırdığını görmek için kurar: değişen özelliği okumayan görünüm atlanır.

**DR9.** Bu ölçüm nesne düzeyinde bildirimin kuralını model olarak kurar. Gerçek `ObservableObject` nesnesinin yayımlama biçimi ve görünümün aynı değerle gelen yayında yeniden hesaplanıp hesaplanmadığı ölçülmedi.

#### Özet

- Gözlemlenebilir nesnenin özelliğini okuyan görünüm, özellik değişince gözlem tetiğiyle yeniden hesaplanır.
- Gözlem özellik düzeyindedir: yalnız okunan özelliğin değişimi bildirim üretir, okunmayan özellik üretmez.
- Gerçek izleme bir kezliktir; ikinci değişikliği görmek için izleme yeniden kurulur.
- Ekranın kendi durumu nesne özelliğinden ayrıdır ve yerel ya da ebeveyn tetik üretir.
- Nesne düzeyinde bildirim, nesneyi izleyen her görünümü her değişiklikte yeniden hesaplatır; özellik düzeyinde gözlem bunu ayırır.
- Bu dersin tetiği: **gözlem**.

#### Sonraki Adım

Bu derste nesneyi çocuklara parametre olarak verdik; ekran nesneyi tuttu, çocuk onu aldı. Nesneyi ağacın her görünümüne tek tek taşımak, aradaki görünümleri yalnızca iletmek için nesneyi tanımaya zorlar. Sıradaki derste değer ağaç boyunca bir kez konur ve altındaki her görünüm onu doğrudan okur. Tetik bu kez ortamdır.

### Ortam Değerleri

Source: https://academia.sh/tr/kurslar/swiftui/ortam-degerleri

Ortam değerinin ağaç boyunca ara görünümlere dokunmadan yalnız okuyanı yeniden hesaplattığının, ortama konan nesnenin özelliğinin ortam değil gözlem tetiklediğinin ve alt ağaçta ezmenin modelde nasıl kurulduğunun ölçümü.

Önceki derste bir nesne ayrı yaşıyordu ve ona ulaşmak için nesneyi her çocuğa parametre olarak verdik. Aradaki görünümler nesneyle ilgilenmese bile onu taşımak zorundaydı. **Ortam** bu taşımayı ortadan kaldırır: değer ağacın bir noktasında konur ve altındaki her görünüm onu doğrudan okuyabilir. Bu derste bakılan tetik **ortam**.

Ölçüm yine çekirdek dosyayla yapılır.

```swift
// 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.environment` bir değerin hiçbir görünüme ait olmadığını, ağacın o noktasından aşağı akan bir yayın olduğunu söyler. `cause` işlevi bu anahtarı okuyan her görünüme sahiplik sorusu sormadan `ortam` tetiğini verir. Model, çerçevenin bağımlılık izleme kuralını kurar; çerçevenin kendisi değildir.

#### Derindeki Okuyan

Çerçeve bazı ortam değerlerini kendisi sağlar: renk kipi, yazı boyutu ayarı, yerel ayar. Uygulama kendi anahtarlarını da ekleyebilir. Özel bir anahtar `EnvironmentValues` üzerine bir uzantıyla tanımlanır ve `@Entry` makrosu varsayılan değeriyle birlikte anahtarı kurar. Bir görünüm değeri `@Environment` ile okur; değer bir üst görünümde `.environment` değiştiricisiyle konur.

```swift
import SwiftUI

extension EnvironmentValues {
    @Entry var textScale: Int = 1
}

struct ReaderScreen: View {
    var body: some View {
        HStack {
            ArticleView()
            SidebarView()
        }
        .environment(\.textScale, 2)
    }
}

struct ArticleView: View {
    var body: some View {
        VStack {
            ParagraphView()
            CaptionView()
        }
    }
}

struct ParagraphView: View {
    @Environment(\.textScale) private var textScale

    var body: some View {
        Text("Olcek \(textScale)")
    }
}

struct CaptionView: View {
    @Environment(\.textScale) private var textScale

    var body: some View {
        Text("Alt yazi \(textScale)")
    }
}

struct SidebarView: View {
    @Environment(\.colorScheme) private var colorScheme

    var body: some View {
        Text(colorScheme == .dark ? "Koyu" : "Acik")
    }
}
```

`ArticleView` hiçbir ortam değerini okumaz; yalnız iki çocuğu barındırır. `ParagraphView` ve `CaptionView` özel `textScale` anahtarını, `SidebarView` ise sistemin `colorScheme` değerini okur. Ağaçta bu üç okuma `reads` içindedir. İki değişiklik verilir: biri özel anahtarın değeri, biri sistemin renk kipi. İlki uygulamanın `.environment` değiştiricisinin değerini değiştirmesiyle, ikincisi kullanıcının sistem ayarını değiştirmesiyle olur; ikisi de ortam değeridir.

```swift
// main.swift — ortam degeri araya dokunmadan yalniz okuyani yeniden hesaplatir
let storage: [String: Storage] = [
    "textScale": .environment,
    "colorScheme": .environment,
]

let screen = ViewTree(nodes: [
    ViewNode("ReaderScreen", parent: nil, reads: []),
    ViewNode("ArticleView", parent: "ReaderScreen", reads: []),
    ViewNode("ParagraphView", parent: "ArticleView", reads: ["textScale"]),
    ViewNode("CaptionView", parent: "ArticleView", reads: ["textScale"]),
    ViewNode("SidebarView", parent: "ReaderScreen", reads: ["colorScheme"]),
], storage: storage)

let recomputes = [
    screen.change("olcek-degisti", key: "textScale"),
    screen.change("kip-degisti", key: "colorScheme"),
].flatMap { $0 }

countCauses(recomputes)
```

```
olcek-degisti: textScale degisti | yeniden ParagraphView ortam, CaptionView ortam | atlanan ReaderScreen,ArticleView,SidebarView
kip-degisti: colorScheme degisti | yeniden SidebarView ortam | atlanan ReaderScreen,ArticleView,ParagraphView,CaptionView
yerel: 0
ebeveyn: 0
gozlem: 0
ortam: 3
```

`olcek-degisti` satırında iki görünüm, `ParagraphView` ve `CaptionView`, yeniden hesaplandı; ikisi de torundur; ata `ReaderScreen` ise değeri koyan görünümdür. `ArticleView` aradaydı ve atlandı: ortam değerini okumadığı için değişiklik ona dokunmadı. `kip-degisti` satırında yalnız `SidebarView` yeniden hesaplandı. Üç yeniden hesaplamanın üçü de ortam; sayımda ortam üç, geri kalanlar sıfır.

Ortamın getirisi bu atlanan satırdadır. Değer ağacın iki seviye altına parametreyle taşınsaydı, `ArticleView` bunu parametre olarak alıp yeniden iletmek zorunda kalırdı ve değer değişince kendisi de okuyan sayılırdı. Ortamda aradaki görünüm değeri bilmez, dolayısıyla değişimden etkilenmez.

**DR10.** Bu ölçüm iki ortam anahtarı ve iki değişiklik için. Sistemin renk kipi gerçekte kullanıcının ayarıyla değişir; ölçümde ayar bir anahtar değişikliği olarak temsil edildi ve ayarın değişimini çerçevenin nasıl ilettiği ölçülmedi.

#### Ortama Konan Nesne

Ortama yalnız basit değerler konmaz; bir gözlemlenebilir nesne de konabilir. Nesne `.environment(session)` ile verilir ve `@Environment(SessionStore.self)` ile okunur. Bu durumda iki ayrı şey değişebilir: nesnenin kendisi ya da nesnenin bir özelliği.

```swift
import SwiftUI
import Observation

@Observable
final class SessionStore {
    var unreadCount = 0
}

struct InboxBadge: View {
    @Environment(SessionStore.self) private var session

    var body: some View {
        Text("Okunmamis: \(session.unreadCount)")
    }
}
```

`InboxBadge` nesneyi ortamdan alır, ama gövdesinde okuduğu şey nesnenin `unreadCount` özelliğidir. Dolayısıyla izlenen şey, ortamın kendisi değil, bir nesne özelliğidir. Ağaçta bu iki ayrım iki farklı saklama olarak yazılır: `textScale` ortam, `unreadCount` bir nesne özelliği.

```swift
// main.swift — ortama konan nesnenin ozelligi ortam degil gozlem tetikler
let storage: [String: Storage] = [
    "textScale": .environment,
    "unreadCount": .model(object: "SessionStore"),
]

let screen = ViewTree(nodes: [
    ViewNode("ReaderScreen", parent: nil, reads: []),
    ViewNode("ParagraphView", parent: "ReaderScreen", reads: ["textScale"]),
    ViewNode("InboxBadge", parent: "ReaderScreen", reads: ["unreadCount"]),
], storage: storage)

let recomputes = [
    screen.change("olcek-degisti", key: "textScale"),
    screen.change("okunmamis-degisti", key: "unreadCount"),
].flatMap { $0 }

countCauses(recomputes)
```

```
olcek-degisti: textScale degisti | yeniden ParagraphView ortam | atlanan ReaderScreen,InboxBadge
okunmamis-degisti: unreadCount degisti | yeniden InboxBadge gozlem | atlanan ReaderScreen,ParagraphView
yerel: 0
ebeveyn: 0
gozlem: 1
ortam: 1
```

İki değişiklik iki yeniden hesaplama üretti ve tetikler farklı: `olcek-degisti` ortam, `okunmamis-degisti` gözlem. `InboxBadge` ortam yoluyla ulaştığı nesneyi okuyor, ama onu yeniden hesaplatan şey ortam değil, nesne özelliğidir. Ortamdan alınan nesne yalnızca erişim yoludur; tetiği değerin saklandığı yer belirler. Bu, üçüncü dersin kuralıyla örtüşür: nesne özelliğini kim ve nasıl okursa okusun tetik gözlemdir.

Sayılar: gözlem bir, ortam bir.

**DR11.** Bu ölçüm yalnız nesnenin özelliğinin değişimi için. Ortamdaki nesnenin tamamen başka bir nesneyle değiştirildiği durum, yani ortam değerinin kendisinin değişimi, ölçülmedi; o durumda ortam tetiği beklenir ama model bunu bir anahtar değişikliği olarak ayrıca temsil etmedi.

#### Sınırlayıcı Ölçüm

Ortam değeri ağacın bir noktasında **ezilebilir**: alt ağaçta aynı anahtara başka bir değer konur ve o alt ağaçtaki görünümler üst değeri değil ezilen değeri okur. Üstteki değerin değişimi bu alt ağacı ilgilendirmez. Model ise her anahtar için tek bir saklama satırı tutar; aynı anahtarın iki farklı değeri olamaz.

```swift
import SwiftUI

struct ReaderScreen: View {
    var body: some View {
        HStack {
            ArticlePane()
            SidePane()
                .environment(\.textScale, 3)
        }
        .environment(\.textScale, 2)
    }
}
```

Aşağıdaki ölçümde ezme bu yüzden iki ayrı anahtarla temsil edilir: `textScale` üst değeri, `paneTextScale` yan bölmedeki ezilen değeri taşır. Hangi görünümün hangisini okuduğunu kaynağa bakarak biz yazıyoruz; model bunu kendisi bulamaz.

```swift
// main.swift — alt agacta ezme modelde ayri bir anahtardir
let storage: [String: Storage] = [
    "textScale": .environment,
    "paneTextScale": .environment,
]

let screen = ViewTree(nodes: [
    ViewNode("ReaderScreen", parent: nil, reads: []),
    ViewNode("ArticlePane", parent: "ReaderScreen", reads: ["textScale"]),
    ViewNode("SidePane", parent: "ReaderScreen", reads: ["paneTextScale"]),
], storage: storage)

let recomputes = [
    screen.change("ust-olcek-degisti", key: "textScale"),
    screen.change("yan-olcek-degisti", key: "paneTextScale"),
].flatMap { $0 }

countCauses(recomputes)
```

```
ust-olcek-degisti: textScale degisti | yeniden ArticlePane ortam | atlanan ReaderScreen,SidePane
yan-olcek-degisti: paneTextScale degisti | yeniden SidePane ortam | atlanan ReaderScreen,ArticlePane
yerel: 0
ebeveyn: 0
gozlem: 0
ortam: 2
```

Üst değerin değişimi yalnız `ArticlePane`'i, ezilen değerin değişimi yalnız `SidePane`'i yeniden hesaplattı. İki satır da ortam. Ama bu sonucu ölçüm değil, kurduğumuz iki anahtar belirledi: ağaçta `SidePane`'e `textScale` okuması yazsaydık, model ezmeyi tanımadığı için üst değişimde onu da yeniden hesaplatırdı. Yani model ezmeyi **temsil edebilir**, ama onu **ayırt edemez**. Gerçek çerçevede ezmenin üst değeri gölgelediği, belgelenmiş bir davranıştır; bu derste ölçülen bir şey değildir.

**DR12.** Bu ölçüm yalnız iki anahtarlı bir temsil içindir. Aynı anahtarın üst ve alt ağaçta iki değerle okunduğu durum, ezmenin birden çok katmanda iç içe geçtiği durum ve ezilen değerin üst değerden türetildiği durum ölçülmedi.

#### Özet

- Ortam değeri ağacın bir noktasında konur; altındaki okuyan görünüm onu aradaki görünümlere dokunmadan okur.
- Değer değişince yalnız onu okuyan görünüm yeniden hesaplanır, tetik ortamdır; okumayan ara görünüm atlanır.
- Sistemin ortam değeri ile özel `@Entry` anahtarı aynı tetiği üretir.
- Ortama konan nesnenin özelliği değişince tetik ortam değil gözlemdir; ortam yalnız erişim yoludur.
- Alt ağaçta ezme, tek anahtarlı modelde ayrı bir anahtar olarak temsil edilir; model ezmeyi ayırt edemez.
- Bu dersin tetiği: **ortam**.

#### Sonraki Adım

Dört tetiğin dördü de bu noktada ölçüldü ve hepsinde değişen şey bir değerdi. Sıradaki derste ekranın kendisi de bir değere bağlanır: hangi ekranın görüneceğini gösteren bir yol. Gezinme yığıtı ve yolu bir durumdur; bu durumun değişimi hangi görünümleri yeniden hesaplatır sorusu, yine aynı tetik sorusudur.

### Gezinme Yığıtı ve Yolu

Source: https://academia.sh/tr/kurslar/swiftui/gezinme-yigiti-ve-yolu

Gezinme yolunun kökte tutulan bir durum olduğunun, itme ve geri dönmenin aynı tetiği ürettiğinin ve yola itilen değerin bir görünüm olmadığının ölçümü.

Önceki dört ders durumun nerede yaşadığına baktı: görünümde, bir bağlamanın ucunda, bir nesnede ve ortamda. Bu derste durumun gösterdiği şey değişir. Ekranlar arasında ilerlemek, hangi ekranın görüneceğini söyleyen bir değerin değişmesidir ve bu değer de yukarıdaki saklama yerlerinden birinde durur. **Gezinme yığıtı** (navigation stack), kullanıcının açtığı ekranları üst üste tutan yapıdır; **yol** ise bu yığıtın içeriğini tarif eden değerler dizisidir. Bu derste bakılan tetik **yerel**: yolu tutan kök görünüm değerin sahibi.

Ölçüm çekirdek dosyayla yapılır.

```swift
// 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)")
    }
}
```

Çekirdekte gezinmeye özgü bir tip yoktur. Yol, `Storage.state(owner:)` ile bir görünümün durumu olarak yazılır; yolu okuyan görünümler `reads` içinde `path` anahtarını taşır. Model, çerçevenin bağımlılık izleme kuralını kurar; çerçevenin kendisi değildir.

#### Yol Kökte Durur

Bir gezinme yığıtı iki parçadan oluşur. Birincisi kökteki `NavigationStack`: yol durumunu ona bir bağlamayla veririz. İkincisi yoldaki her değerin hangi görünüme karşılık geldiğini söyleyen `.navigationDestination(for:destination:)` değiştiricisi. Aşağıdaki kaynak parçada yol bir tamsayı dizisidir ve her tamsayı bir öğenin kimliğidir.

```swift
import SwiftUI

struct RootScreen: View {
    @State private var path: [Int] = []
    @State private var store = ItemStore()

    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                ItemList(path: $path)
                SettingsButton()
            }
            .navigationDestination(for: Int.self) { itemID in
                ItemDetail(itemID: itemID, store: store, path: $path)
            }
        }
    }
}

struct ItemList: View {
    @Binding var path: [Int]

    var body: some View {
        Button("Ac, derinlik \(path.count)") {
            path.append(7)
        }
    }
}
```

Yol `RootScreen`'e aittir. Gövde yolu `NavigationStack`'e bir bağlamayla verir ve alt görünümlere de aynı bağlamayı iletir. `ItemList` bu bağlama üzerinden yolu hem okur hem yazar; derinliği düğmenin yazısında gösterir. `SettingsButton` ise yola dokunmaz. Ağaç, kaynak parçadaki tip adlarını ve özellik adını taşır. İki değişiklik verilir: bir ekranın itilmesi ve geri dönülmesi.

```swift
// main.swift — gezinme yolu kokte durur, itme ve geri donme ayni tetigi uretir
let storage: [String: Storage] = [
    "path": .state(owner: "RootScreen"),
]

let screen = ViewTree(nodes: [
    ViewNode("RootScreen", parent: nil, reads: ["path"]),
    ViewNode("ItemList", parent: "RootScreen", reads: ["path"]),
    ViewNode("SettingsButton", parent: "RootScreen", reads: []),
], storage: storage)

let recomputes = [
    screen.change("ekran-itildi", key: "path"),
    screen.change("geri-donuldu", key: "path"),
].flatMap { $0 }

countCauses(recomputes)
```

```
ekran-itildi: path degisti | yeniden RootScreen yerel, ItemList ebeveyn | atlanan SettingsButton
geri-donuldu: path degisti | yeniden RootScreen yerel, ItemList ebeveyn | atlanan SettingsButton
yerel: 2
ebeveyn: 2
gozlem: 0
ortam: 0
```

İki değişiklik, dört yeniden hesaplama. İki satır birebir aynı: `RootScreen` yerel, `ItemList` ebeveyn, `SettingsButton` atlandı. İtmeyi bir düğmeye dokunan kullanıcı, geri dönmeyi sistemin geri hareketi başlatır; değişikliği yazan her seferinde farklı, ama yolun sahibi aynı, bu yüzden sonuç da aynı. Bu, ikinci dersin kuralının bir uygulamasıdır: kural yazanı okumaz. Sayımda yerel iki, ebeveyn iki, gözlem ve ortam sıfır.

Yolun kökte tutulması bir tasarım kararıdır. Yol bir alt görünümde tutulsaydı, kökün ve yolu okumayan kardeşlerin ona erişimi olmazdı; durumu ataya kaldırmanın nedeni budur. Gezinme yolu en baştan kökte durur, çünkü hangi ekranın gösterileceği kararı tüm ekranlara bağlıdır.

**DR13.** Bu ölçüm iki değişiklik ve tek bir yol anahtarı için. Yolun birden çok değerinin tek seferde değiştiği durum, örneğin tüm yığıtın boşaltılması, ölçülmedi. `NavigationStack`'in yola yazılan değere karşılık gelen hedefi nasıl bulduğu da ölçülmedi.

#### Hedefin Gösterdiği Nesne

Yol bir değer dizisidir: tamsayıdır, görünüm değildir. Yola bir değer itildiğinde çerçeve `.navigationDestination` ile tanımlanan kapanışı o değerle çağırır ve ortaya çıkan görünümü yığıta koyar. Hedef görünüm gerektiğinde bir nesnenin özelliğini de okur. Aşağıdaki kaynak parçada hedef, öğenin başlığını bir gözlemlenebilir nesneden okuyan `ItemDetail` olsun; yol ona bağlama olarak da iletiliyor.

```swift
import SwiftUI
import Observation

@Observable
final class ItemStore {
    var itemTitle = "Taslak"
}

struct ItemDetail: View {
    let itemID: Int
    let store: ItemStore
    @Binding var path: [Int]

    var body: some View {
        VStack {
            Text("\(itemID): \(store.itemTitle)")
            Button("Derinlik \(path.count)") { path.append(itemID + 1) }
        }
    }
}
```

`ItemDetail` iki şey okur: yolun uzunluğunu ve nesnenin başlığını. Bu hedef ekranı artık açıktır ve yığıtta durmaktadır; ağaç `RootScreen`, `ItemList` ve `ItemDetail` üçlüsünü kurar. `ItemDetail` kökün altında yer alır, çünkü hedef kapanışını kök kurar.

```swift
// main.swift — hedefin gosterdigi nesne gozlem, hedefin yolu okumasi ebeveyn uretir
let storage: [String: Storage] = [
    "path": .state(owner: "RootScreen"),
    "itemTitle": .model(object: "ItemStore"),
]

let screen = ViewTree(nodes: [
    ViewNode("RootScreen", parent: nil, reads: ["path"]),
    ViewNode("ItemList", parent: "RootScreen", reads: ["path"]),
    ViewNode("ItemDetail", parent: "RootScreen", reads: ["path", "itemTitle"]),
], storage: storage)

let recomputes = [
    screen.change("baslik-degisti", key: "itemTitle"),
    screen.change("ikinci-ekran-itildi", key: "path"),
].flatMap { $0 }

countCauses(recomputes)
```

```
baslik-degisti: itemTitle degisti | yeniden ItemDetail gozlem | atlanan RootScreen,ItemList
ikinci-ekran-itildi: path degisti | yeniden RootScreen yerel, ItemList ebeveyn, ItemDetail ebeveyn | atlanan yok
yerel: 1
ebeveyn: 2
gozlem: 1
ortam: 0
```

`baslik-degisti` yalnız `ItemDetail`'ı yeniden hesaplattı ve gözlem: başlık bir nesne özelliğidir ve yolu tutan kökle ilgisi yoktur. `ikinci-ekran-itildi` üç görünümü birden yeniden hesaplattı: kök yerel, `ItemList` ve `ItemDetail` ebeveyn. Hedef ekran artık yığıtta olduğu için yolu okuyor ve yol değiştiğinde yeniden hesaplanıyor. Sayılar: yerel bir, ebeveyn iki, gözlem bir.

Burada iki tetik aynı görünümde karşılaşır. `ItemDetail` aynı ölçümde bir kez gözlem, bir kez ebeveyn tetiği aldı. Hedef görünümün hangi değişikliği yaşadığı, onun hangi değeri okuduğuna ve değerin nerede saklandığına bağlıdır; gezinme bunu değiştirmez.

**DR14.** Bu ölçüm yolu okuyan bir hedef ve tek bir nesne özelliği için. Hedefin yolu hiç okumadığı durumda yol değişince yeniden hesaplanmayacağı, ölçülmeden yalnız modelin kuralından çıkar; hedef kapanışının yolu okuyup okumadığı ölçülmedi.

#### Sınırlayıcı Ölçüm

Yola itilen şey bir görünüm değil, bir değerdir. Yolu değiştirmek bir görünümü ağaca eklemez; çerçeve değere karşılık gelen hedefi kurar. Bu yüzden hedef ilk göründüğü anda **yeni bir düğümdür** ve o ilk görünüşü bir yeniden hesaplama değildir. Yeniden hesaplama, daha önce var olan bir görünümün bir değer değişince gövdesinin yeniden okunmasıdır. Aşağıdaki ölçüm aynı değişikliği iki ağaçta uygular: hedefin henüz yığıtta olmadığı ve olduğu.

```swift
// main.swift — yola itilen deger gorunum degildir, hedef yeni dugumdur
let storage: [String: Storage] = [
    "path": .state(owner: "RootScreen"),
]

let beforePush = ViewTree(nodes: [
    ViewNode("RootScreen", parent: nil, reads: ["path"]),
    ViewNode("ItemList", parent: "RootScreen", reads: ["path"]),
], storage: storage)

let afterPush = ViewTree(nodes: [
    ViewNode("RootScreen", parent: nil, reads: ["path"]),
    ViewNode("ItemList", parent: "RootScreen", reads: ["path"]),
    ViewNode("ItemDetail", parent: "RootScreen", reads: ["path"]),
], storage: storage)

let recomputes = [
    beforePush.change("hedef-yokken-itildi", key: "path"),
    afterPush.change("hedef-varken-itildi", key: "path"),
].flatMap { $0 }

countCauses(recomputes)
```

```
hedef-yokken-itildi: path degisti | yeniden RootScreen yerel, ItemList ebeveyn | atlanan yok
hedef-varken-itildi: path degisti | yeniden RootScreen yerel, ItemList ebeveyn, ItemDetail ebeveyn | atlanan yok
yerel: 2
ebeveyn: 3
gozlem: 0
ortam: 0
```

İlk satırda `ItemDetail` ne yeniden hesaplananlar arasında yer alıyor ne de atlananlar arasında. Ağaçta o anda böyle bir düğüm yoktu; itme onu oluşturacak, ama model yeni oluşan düğümün ilk gövdesini bir tetik olarak saymaz. İkinci satırda hedef zaten yığıttaydı ve yolu okuyordu, bu yüzden ebeveyn tetikle yeniden hesaplandı. Sayımda yerel iki, ebeveyn üç; ilk satırın itme eylemi hedefin ilk görünüşünü saymadı.

Bu ayrım ders için bir uyarıdır: gezinmede "ekran açıldı" ile "ekran yeniden hesaplandı" aynı olay değildir. Ekranın ilk açılışı bir kuruluştur; yeniden hesaplama ancak ekran var olduktan sonra, okuduğu değer değişince olur.

**DR15.** Bu ölçüm hedefin var olmadığı ve olduğu iki durumu karşılaştırır. Hedefin ilk gövde hesabının çerçevede kaç adımda yapıldığı ve hedefin yığıttan çıkarılıp yeniden kurulmasında durumunun korunup korunmadığı ölçülmedi.

#### Özet

- Gezinme yolu, kökte tutulan bir durumdur; sahibi kök görünümdür.
- Yol değişince yolu tutan kök yerel, yolu okuyan alt görünümler ebeveyn tetiğiyle yeniden hesaplanır.
- Ekran itmek ile geri dönmek aynı yeniden hesaplama kümesini üretir; yazanın kim olduğu tetiği değiştirmez.
- Hedef görünümün gösterdiği nesne özelliği gözlem, okuduğu yol ebeveyn tetiği üretir.
- Yola itilen şey görünüm değil değerdir; hedef yeni bir düğümdür ve ilk görünüşü yeniden hesaplama sayılmaz.
- Bu dersin tetiği: **yerel**.

#### Sonraki Adım

Bu derste yol bir ekranı yığıta ekledi ve ekran yığıtın bir parçası oldu. Her ekran yığıta girmez: bazıları mevcut ekranın üzerine geçici olarak açılır ve iş bitince kapanır. Sıradaki derste bir sayfa, bir uyarı ve bir tam ekran tabaka da yine bir değere bağlıdır; sunum durumu ve kapatma, yola benzer ama başka bir sahiplikle çalışır.

### Sunum ve Kapatma

Source: https://academia.sh/tr/kurslar/swiftui/sunum-ve-kapatma

Sunum durumunun sunan görünümde durduğunun, sayfa içeriğinin bağlamayla bu durumu okuduğunun ve kapatma eyleminin bir değer olmadığının ölçümü.

Gezinme yığıtı yeni bir ekranı yığıta ekler ve mevcut ekranın yerine geçirir; sunum ise yeni ekranı mevcut ekranın üzerine geçici olarak açar. Bir **sayfa** (sheet), bir **tam ekran tabaka** ya da bir **uyarı** (alert) kendi başına var olmaz: açık olup olmadığını bir değer söyler ve bu değer, tıpkı gezinme yolu gibi, bir saklama yerinde durur. Bu derste bakılan tetik yine **yerel**: sunum durumunu tutan görünüm, değerin sahibi olarak yeniden hesaplanır. Ders ayrıca kapatmanın nasıl işlediğine bakar, çünkü kapatma eylemi başka bir saklama yerinden, ortamdan gelir.

Ölçüm çekirdek dosyayla yapılır.

```swift
// 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)")
    }
}
```

Çekirdekte sunuma özgü bir tip yoktur. Sunum durumu bir `Bool` ya da bir seçenek değeridir ve modelde bir `state` anahtarı olarak yazılır; sayfanın içeriği bu anahtarı okuyorsa `reads` kümesinde taşır.

#### Sunum Durumu Sunanda Durur

Bir sayfa iki parçayla açılır: sunan görünümde tutulan bir `Bool` ve `.sheet(isPresented:content:)` değiştiricisi. Değiştirici bağlamayı alır, değer doğru olduğunda içerik kapanışından bir görünüm kurar ve değer yanlışa dönünce onu kaldırır. Aşağıdaki kaynak parçada sunan `InvoiceScreen`'dir.

```swift
import SwiftUI

struct InvoiceScreen: View {
    @State private var isEditorPresented = false

    var body: some View {
        VStack {
            InvoiceHeader()
            Button("Duzenle") { isEditorPresented = true }
        }
        .sheet(isPresented: $isEditorPresented) {
            InvoiceEditor()
        }
    }
}

struct InvoiceHeader: View {
    var body: some View {
        Text("Fatura 2041")
    }
}
```

Durum `InvoiceScreen`'dedir ve değiştirici de aynı görünümün gövdesine yazılır. `InvoiceHeader` sunuma hiç bağlı değildir. `InvoiceEditor` kapanışın içinde kurulur. Ağaçta sunan ve başlık yer alır; sayfa açıkken içerik de ağaca girer. Önce sunumun kendisini, içerik olmadan ölçeriz: açma ve kapama, yazanı değil, sahibi ilgilendirir.

```swift
// main.swift — sunum durumu sunanda durur, acma ve kapama ayni tetigi uretir
let storage: [String: Storage] = [
    "isEditorPresented": .state(owner: "InvoiceScreen"),
]

let screen = ViewTree(nodes: [
    ViewNode("InvoiceScreen", parent: nil, reads: ["isEditorPresented"]),
    ViewNode("InvoiceHeader", parent: "InvoiceScreen", reads: []),
], storage: storage)

let recomputes = [
    screen.change("sayfa-acildi", key: "isEditorPresented"),
    screen.change("sayfa-kapandi", key: "isEditorPresented"),
].flatMap { $0 }

countCauses(recomputes)
```

```
sayfa-acildi: isEditorPresented degisti | yeniden InvoiceScreen yerel | atlanan InvoiceHeader
sayfa-kapandi: isEditorPresented degisti | yeniden InvoiceScreen yerel | atlanan InvoiceHeader
yerel: 2
ebeveyn: 0
gozlem: 0
ortam: 0
```

İki değişiklik, iki satır, ikisi de yalnız sunanı yeniden hesaplattı. `InvoiceHeader` iki seferde de atlandı: sunum anahtarını okumuyor. Sunum durumu görünümün kendi durumudur ve yerel tetik üretir. Sayım yerel iki, ebeveyn, gözlem ve ortam sıfır.

Bu, ikinci dersin kuralının ve yerel durumun yalın hâlidir. Açık bir sayfa, ağacı genişletir ama genişleme bir yeniden hesaplama sayılmaz; sayfanın içeriği kurulduğunda ilk gövdesini hesaplar, bu kuruluştur.

**DR16.** Bu ölçüm iki değişiklik ve tek bir `Bool` anahtarı için. Seçenek tipli sunumun (`item:` kullanan sayfa) ve aynı görünümde birden çok sunumun birlikte kullanıldığı durum ölçülmedi. Sayfanın ekranda belirme süresi de ölçülmedi.

#### Sayfa İçeriği Bağlamayla Okur

Sayfa içeriğinin sunum durumunu okuması yaygındır: bir kapat düğmesi, sunum açıkken değişen bir başlık. Bunun için içerik, durumu bir bağlama olarak alır; böylece hem okur hem yazar. Bu, ikinci dersin ebeveyn tetiğidir: durumun sahibi sunan, okuyan ise sayfanın içeriğidir.

```swift
import SwiftUI

struct InvoiceEditor: View {
    @Binding var isPresented: Bool

    var body: some View {
        VStack {
            Text(isPresented ? "Duzenleniyor" : "Kapaniyor")
            Button("Bitti") { isPresented = false }
        }
    }
}
```

Sunan görünüm artık içeriği `InvoiceEditor(isPresented: $isEditorPresented)` olarak kurar. Ölçümde ağaç üç görünümden oluşur: sunan, başlık ve içerik. Aynı iki değişiklik uygulanır.

```swift
// main.swift — sayfa icerigi sunum anahtarini baglamayla okur
let storage: [String: Storage] = [
    "isEditorPresented": .state(owner: "InvoiceScreen"),
]

let screen = ViewTree(nodes: [
    ViewNode("InvoiceScreen", parent: nil, reads: ["isEditorPresented"]),
    ViewNode("InvoiceHeader", parent: "InvoiceScreen", reads: []),
    ViewNode("InvoiceEditor", parent: "InvoiceScreen", reads: ["isEditorPresented"]),
], storage: storage)

let recomputes = [
    screen.change("sayfa-acildi", key: "isEditorPresented"),
    screen.change("sayfa-kapandi", key: "isEditorPresented"),
].flatMap { $0 }

countCauses(recomputes)
```

```
sayfa-acildi: isEditorPresented degisti | yeniden InvoiceScreen yerel, InvoiceEditor ebeveyn | atlanan InvoiceHeader
sayfa-kapandi: isEditorPresented degisti | yeniden InvoiceScreen yerel, InvoiceEditor ebeveyn | atlanan InvoiceHeader
yerel: 2
ebeveyn: 2
gozlem: 0
ortam: 0
```

Her iki değişiklikte sunan yerel, içerik ebeveyn tetiğiyle yeniden hesaplanıyor; başlık atlanıyor. Sayım yerel iki, ebeveyn iki. İçeriğin ağaçta sunanın çocuğu olarak durması, ebeveyn tetiğinin koşuludur: sahip, okuyanın atasıdır.

Burada bir ayrıntıya dikkat etmek gerekir. Sayfanın içeriği sunanın çocuğu olarak modellenmiştir, çünkü içerik kapanışını sunan kurar. Sayfa ekranda ayrı bir katmanda görünse de durumun sahipliği bakımından ağaçta sunanın altındadır.

**DR17.** Bu ölçüm içeriğin sunanın doğrudan çocuğu olduğu tek bir ağaç için. İçeriğin birkaç kat aşağıda başka görünümlere bağlamayı ilettiği durum ve sayfanın kendi içinde ayrıca bir gezinme yığıtı taşıdığı durum ölçülmedi.

#### Sınırlayıcı Ölçüm

Sayfayı kapatmanın iki yolu vardır: içeriğin sunum bağlamasına `false` yazması ya da ortamdan gelen **kapatma eylemini** (`dismiss`) çağırması. İkincisi, `@Environment(\.dismiss)` ile okunur. Bu bir değer değil, çağrılabilir bir eylemdir; çağırmak çerçeveye "beni sunan durumu kapat" der ve çerçeve sunanın durumunu yazar. Eylemin kendisi hiçbir değişikliği tek başına üretmez. Ortamdan eylemi okumak, ortam değerinin değişmesini beklemek anlamına gelmez, çünkü eylemin kendisi sabittir.

```swift
import SwiftUI

struct InvoiceEditor: View {
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        Button("Kapat") { dismiss() }
    }
}
```

Ölçüm kapatma çağrısının gerçekte yaptığını modeller. `dismiss` ağaçta içeriğin okuduğu bir ortam anahtarı olarak yer alır ve değiştirilmez, çünkü eylemin kendisi sabittir; çağrıyla değişen şey sunum anahtarıdır.

```swift
// main.swift — dismiss deger degil eylemdir, kapanma sunanin durumunu degistirir
let storage: [String: Storage] = [
    "dismiss": .environment,
    "isEditorPresented": .state(owner: "InvoiceScreen"),
]

let screen = ViewTree(nodes: [
    ViewNode("InvoiceScreen", parent: nil, reads: ["isEditorPresented"]),
    ViewNode("InvoiceEditor", parent: "InvoiceScreen", reads: ["dismiss"]),
], storage: storage)

let recomputes = [
    screen.change("kapat-cagrildi", key: "isEditorPresented"),
].flatMap { $0 }

countCauses(recomputes)
```

```
kapat-cagrildi: isEditorPresented degisti | yeniden InvoiceScreen yerel | atlanan InvoiceEditor
yerel: 1
ebeveyn: 0
gozlem: 0
ortam: 0
```

Tek satır, kapatma çağrısının sunum anahtarını değiştirmesidir. Bu yalnız sunanı yerel tetikle yeniden hesaplatır; içerik bu anahtarı okumadığı için atlanır. `dismiss` anahtarı değişmediği için ortam sayacı sıfırdır. Sayım yerel bir.

Bu satır dersin sonucunu verir. Kapatma eylemini okumak, içeriği sunum durumuna bağlamaz; çağrı yapıldığında değişen şey ortam değil, sunanın durumudur. Eylem ortamdan geldiği için ağaçta bir ortam anahtarı olarak yer alır, ama çağrının sonucu sunanda görülür. Çağrıyı yapan içerik sunum durumunu okumuyorsa, kapanışın yeniden hesaplamasına da katılmaz.

**DR18.** Ölçüm `dismiss` anahtarını değişmeyen bir ortam anahtarı olarak kurar; çerçevede eylem tanıtıcısının değişip değişmediği ölçülmedi. Kapatma çağrısının sunum durumunu yazma sırası ve animasyonla kapanmanın bitmesini beklemek ölçülmedi.

#### Özet

- Sunum durumu, sunan görünümde tutulan bir değerdir; açma ve kapama aynı tetiği üretir.
- Sunan, durumun sahibi olarak yerel, sayfa içeriği bağlamayla okuyorsa ebeveyn tetiğiyle yeniden hesaplanır.
- Sunumla ilgisi olmayan kardeş görünümler atlanır.
- `dismiss` ortamdan okunan bir eylemdir; çağırmak sunanın durumunu değiştirir, eylemin kendisi bir değer değildir.
- Bu dersin tetiği: **yerel**.

#### Sonraki Adım

Bu derste sunum tek bir `Bool` ile yönetildi ve sahibi sunan görünümdü. Uygulamaların çoğunda ise ekran birden çok bölüme ayrılır: bir sekme çubuğu ve bir araç çubuğu, hangi bölümün görüneceğini ve hangi eylemlerin sunulacağını söyler. Sıradaki derste seçim kökte durur; bölümlerin içeriği ona bağlamayla erişse de onu okuyup okumadığı ayrıca ölçülür.

### Sekme ve Araç Çubuğu

Source: https://academia.sh/tr/kurslar/swiftui/sekme-ve-arac-cubugu

Sekme seçiminin kökte durduğunun, seçimi bağlamayla yalnız yazan içeriğin okuyan sayılmadığının ve seçili olmayan sekmenin seçimi okumadığı için atlandığının ölçümü.

Sekme çubuğu ve araç çubuğu, ekranın üstünde ve altında duran iki ayrı kabuktur. Birincisi hangi bölümün göründüğünü, ikincisi o bölümün hangi eylemleri sunduğunu söyler. İkisi de bir değere dayanır. Sekme çubuğunda bu **seçim** (selection), kökte tutulan bir değerdir; araç çubuğunda ise düğmenin etkin olup olmadığı genellikle bir nesnenin özelliğinden gelir. Bu derste bakılan tetik **yerel**: seçim kökte durur ve onu kök okur; sekme içerikleri seçimi bağlamayla alsa da gövdelerinde okumaz.

Ölçüm çekirdek dosyayla yapılır.

```swift
// 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, seçimi `state` anahtarı, düğmenin etkinliğini taşıyan nesne özelliğini `model` anahtarı olarak yazar.

#### Seçim Kökte Durur

Bir `TabView` seçimi bir bağlama olarak alır. Her sekmenin içeriği `.tag` ile bir değer taşır; çerçeve seçim değeriyle etiketi eşleştirip hangi içeriği göstereceğine karar verir. Aşağıdaki kaynak parçada kök, seçimin sahibidir ve içeriklere onu bağlama olarak iletir.

```swift
import SwiftUI

struct ShellScreen: View {
    @State private var selectedTab = 0

    var body: some View {
        TabView(selection: $selectedTab) {
            InboxTab(selectedTab: $selectedTab)
                .tag(0)
            ArchiveTab()
                .tag(1)
        }
    }
}

struct InboxTab: View {
    @Binding var selectedTab: Int

    var body: some View {
        Button("Arsive git") { selectedTab = 1 }
    }
}

struct ArchiveTab: View {
    var body: some View {
        Text("Arsiv")
    }
}
```

Seçim `ShellScreen`'dedir. `InboxTab` seçimi bağlamayla alır, çünkü bir düğmeyle başka sekmeye geçebilmesi gerekir; ama değeri gövdesinde okumaz, yalnız düğmenin eyleminde yazar. İkinci dersin kuralı burada da geçerlidir: yalnız yazan görünüm okuyanlar arasına girmez. `ArchiveTab` seçimi hiç almaz. Önce kök ve iki sekmeyle bir seçim değişikliği ölçülür: gelen kutusundan arşive, arşivden geri.

```swift
// main.swift — sekme secimi kokte durur, secimi okumayan sekme atlanir
let storage: [String: Storage] = [
    "selectedTab": .state(owner: "ShellScreen"),
]

let screen = ViewTree(nodes: [
    ViewNode("ShellScreen", parent: nil, reads: ["selectedTab"]),
    ViewNode("InboxTab", parent: "ShellScreen", reads: []),
    ViewNode("ArchiveTab", parent: "ShellScreen", reads: []),
], storage: storage)

let recomputes = [
    screen.change("arsive-gecildi", key: "selectedTab"),
    screen.change("gelene-donuldu", key: "selectedTab"),
].flatMap { $0 }

countCauses(recomputes)
```

```
arsive-gecildi: selectedTab degisti | yeniden ShellScreen yerel | atlanan InboxTab,ArchiveTab
gelene-donuldu: selectedTab degisti | yeniden ShellScreen yerel | atlanan InboxTab,ArchiveTab
yerel: 2
ebeveyn: 0
gozlem: 0
ortam: 0
```

İki değişiklik, iki yeniden hesaplama; her satırda yalnız kök yerel, iki sekme atlanan. Sayım yerel iki.

İki sekme de seçim değerini okumuyor; bu yüzden hangi sekmenin göründüğünü bilmeleri gerekmez. Çerçeve seçimi etiketlerle eşleştirir, içeriklerin kendisi bu eşleştirmeye katılmaz. Bir sekmenin içeriği, seçimi gövdesinde okumadığı sürece atlanır. `InboxTab` bağlamayı taşısa da yalnız yazdığı için atlandı; seçimi gövdede okuyan bir içerik olsaydı ebeveyn tetiği alırdı, bu ağaçta öyle bir içerik yok.

**DR19.** Bu ölçüm iki değişiklik ve iki sekme için. Çerçevenin seçili olmayan sekmenin içeriğini hiç kurmayıp kurmadığı ya da bellekte tutup tutmadığı ölçülmedi; ölçüm yalnız hangi görünümün yeniden hesaplandığını sayar.

#### Araç Çubuğu Düğmesi Nesneden Okur

Araç çubuğundaki bir düğmenin etkin olup olmadığı, bölümün gösterdiği nesneye bağlıdır: gelen kutusunda seçili ileti varsa "Sil" düğmesi etkindir. Düğme `.toolbar` içinde tanımlanır ve etkinliğini nesne özelliğinden alır. Bu bir nesne özelliğidir; ikinci dersin bağlamasıyla değil, üçüncü dersin gözlem tetiğiyle ilgilidir.

```swift
import SwiftUI
import Observation

@Observable
final class MailboxStore {
    var hasSelectedMessage = false
}

struct InboxToolbar: ToolbarContent {
    let store: MailboxStore

    var body: some ToolbarContent {
        ToolbarItem {
            Button("Sil") { }
                .disabled(!store.hasSelectedMessage)
        }
    }
}
```

Ölçümde kök, gelen kutusu sekmesi, araç çubuğu düğmesi ve arşiv sekmesi vardır. Nesne özelliğini yalnız araç çubuğu düğmesi okur; seçimi ise yalnız kök okur.

```swift
// main.swift — arac cubugu dugmesinin etkinligi nesne ozelligine baglidir
let storage: [String: Storage] = [
    "selectedTab": .state(owner: "ShellScreen"),
    "hasSelectedMessage": .model(object: "MailboxStore"),
]

let screen = ViewTree(nodes: [
    ViewNode("ShellScreen", parent: nil, reads: ["selectedTab"]),
    ViewNode("InboxTab", parent: "ShellScreen", reads: []),
    ViewNode("DeleteButton", parent: "InboxTab", reads: ["hasSelectedMessage"]),
    ViewNode("ArchiveTab", parent: "ShellScreen", reads: []),
], storage: storage)

let recomputes = [
    screen.change("ileti-secildi", key: "hasSelectedMessage"),
    screen.change("arsive-gecildi", key: "selectedTab"),
].flatMap { $0 }

countCauses(recomputes)
```

```
ileti-secildi: hasSelectedMessage degisti | yeniden DeleteButton gozlem | atlanan ShellScreen,InboxTab,ArchiveTab
arsive-gecildi: selectedTab degisti | yeniden ShellScreen yerel | atlanan InboxTab,DeleteButton,ArchiveTab
yerel: 1
ebeveyn: 0
gozlem: 1
ortam: 0
```

`ileti-secildi` yalnız araç çubuğu düğmesini yeniden hesaplattı ve gözlem: kök, gelen kutusu ve arşiv atlandı. `arsive-gecildi` ise yalnız kökü yerel yeniden hesaplattı; gelen kutusu, düğme ve arşiv seçimi okumuyor, atlandı. Sayım yerel bir, gözlem bir.

İki değişikliğin kümeleri ayrık: biri düğmeye, öbürü yalnız köke dokunuyor. Bu, durumun nerede durduğunun ölçülebilir sonucudur: seçim kökte olduğu için kök de yeniden hesaplanır; etkinlik bir nesnede olduğu için kök hiç dokunulmaz.

**DR20.** Bu ölçüm iki anahtar ve tek bir araç çubuğu düğmesi için. Araç çubuğunun yerleşiminin sekme değişince değişmesi ve düğmenin birden çok sekmede ortak olduğu durum ölçülmedi.

#### Sınırlayıcı Ölçüm

Seçilen sekme değişince önceki sekmenin içeriği ekrandan çıkar, ama bu onun bir yeniden hesaplama aldığı anlamına gelmez. Bir görünümün yeniden hesaplanması için okuduğu değerin değişmesi gerekir. Seçili olmayan sekme seçimi okumuyorsa, seçim her değiştiğinde atlanır. Ölçüm bunu üç sekmeyle gösterir: ilki seçimi bağlamayla alır ama yalnız yazar, öbür ikisi seçimi hiç almaz.

```swift
// main.swift — secili olmayan sekmenin icerigi secimi okumadikca atlanir
let storage: [String: Storage] = [
    "selectedTab": .state(owner: "ShellScreen"),
]

let screen = ViewTree(nodes: [
    ViewNode("ShellScreen", parent: nil, reads: ["selectedTab"]),
    ViewNode("InboxTab", parent: "ShellScreen", reads: []),
    ViewNode("ArchiveTab", parent: "ShellScreen", reads: []),
    ViewNode("SettingsTab", parent: "ShellScreen", reads: []),
], storage: storage)

let recomputes = [
    screen.change("ayarlara-gecildi", key: "selectedTab"),
].flatMap { $0 }

countCauses(recomputes)
```

```
ayarlara-gecildi: selectedTab degisti | yeniden ShellScreen yerel | atlanan InboxTab,ArchiveTab,SettingsTab
yerel: 1
ebeveyn: 0
gozlem: 0
ortam: 0
```

Üç sekmenin üçü de, hem önceki hem sonraki seçimden bağımsız olarak atlandı; bağlamayı taşıyan gelen kutusu sekmesi de bunlara dahil. Yeniden hesaplanan yalnız kök. Sayım yerel bir.

Tasarım sonucu buradan çıkar: bu modelde yeniden hesaplamayı belirleyen, bağlamanın verilmesi değil değerin gövdede okunmasıdır. Seçimi yalnız yazan bir içeriğe bağlama vermek hesaplamayı kökün dışına taşımaz; içerik seçimi gövdesinde okumaya başladığı anda ebeveyn tetiği alır. Bağlamanın kendisinin çerçevede bir maliyeti olup olmadığı ölçülmedi.

**DR21.** Bu ölçüm tek bir seçim değişikliği ve üç sekme için. Seçili olmayan sekmelerin ekrandan çıkarken çerçevenin yaptığı iş, örneğin görünüm durumunun korunması, ölçülmedi.

#### Özet

- Sekme seçimi kökte tutulan bir durumdur; seçim değişince kök yerel tetik alır.
- Seçimi bağlamayla alıp yalnız yazan sekme içeriği okuyan sayılmaz ve atlanır; seçimi gövdede okuyan içerik ebeveyn tetiği alırdı.
- Araç çubuğu düğmesinin etkinliği bir nesne özelliğinden geliyorsa gözlem tetiği üretir ve köke dokunmaz.
- Seçili olmayan bir sekme, seçimi okumadığı sürece her seçim değişikliğinde atlanır.
- Bu dersin tetiği: **yerel**.

#### Sonraki Adım

Bu konu durumun nerede durduğunu, kimi yeniden hesaplattığını ve ekranlar arasında nasıl taşındığını ölçtü. Sıradaki konu parmağın kendisine bakar: bir sürükleme sırasında ofset değeri her karede değişir ve bu değişiklik de aynı zincirin bir halkasıdır. İlk ders, Hareketler, sürükleme ofsetinin hangi görünümü yeniden hesaplattığını ölçer.

## Etkileşim ve Sunum Kalitesi

Dokunma, sürükleme ve birleşik hareketler, örtük ve açık animasyonlar, görünüm ekleme ve kaldırma geçişleri, etiket, özellik ve dinamik yazı boyutuyla erişilebilirlik, metin, tarih ve sayı biçimlemeyle yerelleştirme, ön izlemeyle hızlı geri bildirim döngüsü.

### Hareketler

Source: https://academia.sh/tr/kurslar/swiftui/hareketler

Sürükleme sırasındaki geçici değerin yerel, hareket bitince yazılan kalıcı konumun ebeveyn tetiğiyle yeniden hesaplamaya yol açtığının ölçümü.

Önceki konuda bir değerin hangi yoldan görünüme bağlandığına baktık; kullanıcı bu değerleri çoğunlukla bir hareketle değiştirir. Bu derste soru değişmez: **bu yeniden hesaplamayı ne tetikledi?** Bu derste bakılan tetik **yerel**. Kursun açılış dersinde altı değişiklik sekiz yeniden hesaplama üretmişti; ikisi yerel, ikisi ebeveyn, ikisi gözlem, ikisi ortam tetiğine aitti. Şimdi tek bir sürüklemenin bu dört kategoriden hangilerini kullandığını sayacağız.

Hareket (gesture), kullanıcının ekrana dokunuşunu bir değere çeviren bildirimsel bir tanımdır: dokunma, uzun basma, sürükleme ve bunların birleşimleri aynı biçimde görünüme bir değiştirici olarak eklenir. Sürüklemenin bir özelliği ölçüyü belirler. Parmak ekrandayken konum saniyede birçok kez değişir, ama parmak kalkınca bu ara değerlerin hiçbiri kalıcı olmak zorunda değildir. Çerçeve bunun için ayrı bir özellik sarmalayıcısı sunar: `@GestureState`. Hareket sürerken değer güncellenir, hareket bitince değer kendiliğinden ilk değerine döner. Kalıcı sonuç ise hareketin bitiş geri çağrısında, sıradan bir durum değerine yazılır.

Ölçüm çekirdek dosyayla yapılır; çekirdek her derste aynı dosya adıyla ve aynı içerikle gelir.

```swift
// 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)")
    }
}
```

#### Sürüklemenin İki Değeri

Aşağıdaki parça bir kartı sürükler. Kartı çizen görünüm geçici ofseti kendi hareket durumunda tutar. Kalıcı konum ise bir üst görünümdedir ve karta bağlama ile verilir; konumu bir etiket de okur.

```swift
import SwiftUI

struct BoardScreen: View {
    @State private var position = CGSize.zero

    var body: some View {
        VStack {
            DragCard(position: $position)
            PositionLabel(position: position)
            HintText()
        }
    }
}

struct DragCard: View {
    @Binding var position: CGSize
    @GestureState private var dragOffset = CGSize.zero

    var body: some View {
        RoundedRectangle(cornerRadius: 12)
            .offset(
                x: position.width + dragOffset.width,
                y: position.height + dragOffset.height
            )
            .gesture(
                DragGesture()
                    .updating($dragOffset) { value, state, _ in
                        state = value.translation
                    }
                    .onEnded { value in
                        position.width += value.translation.width
                        position.height += value.translation.height
                    }
            )
    }
}

struct PositionLabel: View {
    let position: CGSize

    var body: some View {
        Text("\(Int(position.width)), \(Int(position.height))")
    }
}

struct HintText: View {
    var body: some View {
        Text("Kartı sürükleyin")
    }
}
```

Bu parça derlenmez ve çıktı vermez; platform çerçevesini içe aldığı için yalnız kaynak olarak durur. Ağacı buradan okuyup düz değerlerle yeniden kuracağız. `DragCard` iki değeri okur: `dragOffset` kendi hareket durumudur, `position` ise ebeveynden gelen bağlamadır. `PositionLabel` yalnız `position`'ı okur. `HintText` hiçbirini okumaz. `BoardScreen` konumun sahibidir ve gövdesinde onu bağlama olarak ve değer olarak iletir; yani gövdesi de konumu okur.

Ölçmeden önce modelin ne olduğunu söyleyelim. Model, çerçevenin bağımlılık izleme kuralını kurar, çerçevenin kendisi değildir: bir görünüm okuduğu değer değişince yeniden hesaplanır, okumadığı değer değişince atlanır. Çerçevenin gövdeyi kaç kez çağırdığı ya da ne zaman çizdiği burada ölçülmez.

Çekirdek bir şey değiştirmez; bir anahtar verildiğinde onu okuyanları bulur ve her biri için tetiği sınıflandırır. Şimdi yukarıdaki kaynak parçadaki tip adlarıyla aynı adları taşıyan ağacı kuralım ve üç değişiklik yazalım: sürükleme sürerken `dragOffset`, hareket bitince `position` ve hareket durumunun ilk değerine dönüşü.

```swift
// main.swift — surukleme sirasinda ve sonrasinda uc degisiklik
let storage: [String: Storage] = [
    "position": .state(owner: "BoardScreen"),
    "dragOffset": .state(owner: "DragCard"),
]

let screen = ViewTree(nodes: [
    ViewNode("BoardScreen", parent: nil, reads: ["position"]),
    ViewNode("DragCard", parent: "BoardScreen", reads: ["position", "dragOffset"]),
    ViewNode("PositionLabel", parent: "BoardScreen", reads: ["position"]),
    ViewNode("HintText", parent: "BoardScreen", reads: []),
], storage: storage)

let recomputes = [
    screen.change("surukleme-suruyor", key: "dragOffset"),
    screen.change("surukleme-bitti-konum", key: "position"),
    screen.change("surukleme-bitti-ofset", key: "dragOffset"),
].flatMap { $0 }

countCauses(recomputes)
```

```
surukleme-suruyor: dragOffset degisti | yeniden DragCard yerel | atlanan BoardScreen,PositionLabel,HintText
surukleme-bitti-konum: position degisti | yeniden BoardScreen yerel, DragCard ebeveyn, PositionLabel ebeveyn | atlanan HintText
surukleme-bitti-ofset: dragOffset degisti | yeniden DragCard yerel | atlanan BoardScreen,PositionLabel,HintText
yerel: 3
ebeveyn: 2
gozlem: 0
ortam: 0
```

Sürükleme sürerken tek görünüm yeniden hesaplanır: kartın kendisi, çünkü `dragOffset` onun hareket durumudur ve yalnız onun gövdesi okur. İki kardeşi ve ebeveyni atlanır. Bu, sürüklemenin ucuz olmasının nedenidir: parmak her kıpırdadığında etiketin ya da ipucu metninin gövdesi yeniden hesaplanmaz.

**EK1.** Bu ölçüm yalnız `dragOffset` değerinin sahibinin `DragCard` olduğu ağaç için geçerlidir. Hareket durumunu bir üst görünüme taşıyan bir düzenin sayıları farklı olurdu; o düzen ölçülmedi.

**EK2.** Bu ölçüm sürükleme sırasında tek bir değişikliği temsil eder. Parmağın bir saniyede kaç ara değer ürettiği ve çerçevenin bu değerleri birleştirip birleştirmediği ölçülmedi.

#### Kalıcı Konum Ebeveyne Yazılır

İkinci satır farklı bir hikâye anlatır. Hareket bitince kalıcı konum `position`'a yazılır ve bu değerin sahibi `BoardScreen`'dir. Sahip kendisi yeniden hesaplanır, yani tetik yereldir. Değeri bağlama ile okuyan `DragCard` ve değeri parametre olarak alan `PositionLabel` ise sahibin altındadır, yani onlar için tetik **ebeveyn**dir: değişikliği kendileri yapmadılar, bir üst görünümün değeri değişti. `HintText` yine atlanır, çünkü hiçbir şey okumaz.

Üçüncü satır, hareket durumunun ilk değerine dönüşüdür. Çerçeve bu dönüşü bizim adımıza yapar, ama model yapmaz: dönüş, modelde yazar tarafından ayrı bir değişiklik olarak eklendi. Kart bu değişiklikte bir kez daha yeniden hesaplanır ve tetik yine yereldir. Bu yüzden son sayım yerel tetik için üç, ebeveyn tetiği için iki yeniden hesaplama verir: üç yerelin ikisi kartındır, biri ekranındır; iki ebeveynin ikisi de konum değişikliğinden gelir. Gözlem ve ortam sayıları sıfırdır, çünkü bir sürükleme bu iki kanalı hiç kullanmaz. Açılış dersindeki 2/2/2/2 dağılımı ile bu dağılımın farkı, değişikliğin türünden gelir: tek bir etkileşim çoğunlukla tek bir ya da iki kanalın içinde kalır.

Bu ayrımın kullanım sonucu şudur. Parmakla birlikte hareket eden değer hareket durumunda tutulursa, o değeri okumayan kardeşler sürükleme boyunca etkilenmez. Aynı değer doğrudan `position` üzerinde güncellenseydi, her ara değer etiketi de yeniden hesaplatırdı. İki değeri ayırmak, çerçevenin sunduğu araca uyan bir tasarımdır ve ölçüm bunu sayıyla gösterir.

**EK3.** Bu ölçüm, `DragCard`, `PositionLabel` ve `HintText` görünümlerinin `BoardScreen`'in doğrudan çocukları olduğu ağaç için geçerlidir. Okuyanların daha derin bir ağaca yerleştiği düzen ölçülmedi.

**EK4.** Bu ölçüm hareketin bitişinde `position` değerinin bir kez yazıldığını varsayar. Bitiş geri çağrısının hangi anda ve hangi iş parçacığında koştuğu ölçülmedi.

#### Dokunma ve Birleşik Hareketler

Sürükleme bu ailenin yalnız bir üyesidir. Dokunma, uzun basma ve sürükleme ayrı hareket tanımlarıdır; bir görünüme tek tek eklenebilir ya da birleştirilebilir. Birleştirmenin üç biçimi vardır: biri aynı anda çalışan hareketleri, biri sırayla çalışan hareketleri, öteki ise yalnız birinin başarılı olduğu ve ilkine öncelik veren hareketleri tanımlar. Aşağıdaki parça, önce uzun basılmasını sonra sürüklenmesini isteyen bir sıra kurar ve ayrıca dokunmayı bir durum değerine bağlar.

```swift
import SwiftUI

struct CardRow: View {
    @State private var isPinned = false

    var body: some View {
        Text("Kart")
            .onTapGesture {
                isPinned.toggle()
            }
            .gesture(
                LongPressGesture()
                    .sequenced(before: DragGesture())
            )
    }
}
```

Bu parçada yeni bir tetik kategorisi yoktur. Dokunma kapanışı sıradan bir durum değerine yazar; değerin sahibi `CardRow` olduğu için değişiklik yereldir ve `isPinned`'i okuyan başka bir görünüm yoksa yalnız satırın kendisi yeniden hesaplanır. Hareketin kendisi bir tetik değildir: tetik, hareketin sonunda yazılan ya da hareket sırasında güncellenen değerdir. Bu ayrım dersin ana fikridir ve ölçümün neden `dragOffset` ve `position` anahtarlarıyla yapıldığını açıklar. Modelde "sürükleme" adlı bir anahtar yoktur; anahtarlar, hareketin dokunduğu saklama yerleridir.

Sıralı bir birleşimde ilk hareket başarılı olmadan ikincisi başlamaz. Bu davranış çerçevenin hareket tanıma kurallarına aittir ve bu derste ölçülmez; model yalnız, birleşimin sonunda hangi değerin yazıldığını sorar.

#### Sınırlayıcı Ölçüm

Modelin neyi ölçemediğini ayrı bir blokta gösterelim. Hareketin sunduğu her bilgi bir saklama yerine bağlanmaz: sürükleme hızı gibi bir değer yalnız geri çağrının içinde okunur ve hiçbir durumda tutulmaz. Modelde böyle bir değerin saklama kaydı yoktur. Okuyan bir görünüm tanımlayıp kaydı vermezsek çekirdek, tetiği sınıflandıramaz.

```swift
// main.swift — saklama kaydi olmayan bir anahtar
let storage: [String: Storage] = [
    "position": .state(owner: "BoardScreen"),
]

let screen = ViewTree(nodes: [
    ViewNode("BoardScreen", parent: nil, reads: ["position"]),
    ViewNode("DragCard", parent: "BoardScreen", reads: ["position", "velocity"]),
], storage: storage)

let recomputes = screen.change("hiz-okundu", key: "velocity")
print(recomputes.count)
```

```
hiz-okundu: velocity degisti | yeniden DragCard olcum-disi | atlanan BoardScreen
1
```

Çekirdek `olcum-disi` yazar ve bu, bir tetik kategorisi değildir; modelin "bu değeri nereden okuyorsun, bilmiyorum" deme biçimidir. Dersin sayıları yalnız dört kategoriden birine düşen değişiklikler için geçerlidir. Hareket durumunun ilk değerine ne zaman döndüğü de aynı sınıftadır: model dönüşü yazar eklediği için sayar, çerçevenin zamanlamasını iddia etmez. Dönüşün bitiş geri çağrısından önce mi sonra mı olduğu ölçülmedi.

#### Özet

- Hareket, kullanıcının ekrana dokunuşunu bir değere çeviren bildirimsel bir tanımdır; geçici değer hareket durumunda, kalıcı sonuç durumda tutulur.
- Sürükleme sürerken değişen hareket durumu yalnız onu okuyan görünümü yeniden hesaplatır; kardeşler atlanır.
- Hareket bitince yazılan konum sahibine yerel, onu bağlama ya da parametreyle okuyan çocuklarına ebeveyn tetiğiyle ulaşır.
- Hareket durumunun ilk değerine dönüşü modelde ayrı bir yerel değişikliktir; çerçevenin zamanlaması ölçülmez.
- Bu dersin tetiği: **yerel**.

#### Sonraki Adım

Bir sürüklemenin sonunda konum değişir ve görünüm yeni yerine geçer; bu geçişin ani mi yoksa yumuşak mı olacağı ayrı bir sorudur. Sonraki derste animasyonla sarılan değişikliğin, sarılmamış aynı değişiklikten tetik açısından ayrılıp ayrılmadığını ölçeceğiz.

### Örtük ve Açık Animasyonlar

Source: https://academia.sh/tr/kurslar/swiftui/ortuk-ve-acik-animasyonlar

Animasyonla sarılan bir değişikliğin, sarılmamış aynı değişiklikle aynı görünümleri aynı tetikle yeniden hesaplattığının ölçümü.

Önceki derste sürükleme bittiğinde konumun değiştiğini ve kartın yeni yerinde durduğunu ölçtük. O değişikliğin görünüme ani mi yoksa ara değerlerden geçerek mi yansıyacağı ayrı bir karardır ve o kararı veren araç animasyondur. Soru yine aynı: **bu yeniden hesaplamayı ne tetikledi?** Bu derste bakılan tetik **yerel**; yanında bir ebeveyn örneği de vardır.

Animasyon, bir değişikliğin görünüme yansımasını zamana yayan bildirimsel bir tanımdır. İki yolla eklenir. Açık animasyon, değişikliği yapan kodu sarar: `withAnimation { isExpanded.toggle() }` yazan kişi, "bu değişiklik animasyonla yansısın" demiş olur. Örtük animasyon ise görünüme bağlanır: `.animation(_:value:)` değiştiricisi, verilen değer değiştiğinde o görünümün altındaki değişikliklere animasyon uygular. İlki değişikliğin bulunduğu yerde, ikincisi değişikliği gören görünümde yazılır. Ama ikisi de bir değeri değiştirmez; yalnız değişikliğin yansıma biçimini seçer.

Ölçüm çekirdek dosyayla yapılır; çekirdek her derste aynı dosya adıyla ve aynı içerikle gelir.

```swift
// 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)")
    }
}
```

#### Aynı Değişiklik, İki Sarmalama

Aşağıdaki parçada bir panel vardır. Panelin ayrıntı alanı, kendi durumu olan `isExpanded` ile açılıp kapanır ve düğme bu değişikliği açık animasyonla sarar. Panelin saydamlığı ise ekranın verdiği `isOn` değerine bağlıdır ve örtük animasyon bu değeri izler.

```swift
import SwiftUI

struct PanelScreen: View {
    @State private var isOn = false

    var body: some View {
        VStack {
            Toggle("Etkin", isOn: $isOn)
            DetailPanel(isOn: isOn)
            StatusText()
        }
    }
}

struct DetailPanel: View {
    let isOn: Bool
    @State private var isExpanded = false

    var body: some View {
        VStack {
            Button("Ayrıntı") {
                withAnimation {
                    isExpanded.toggle()
                }
            }
            if isExpanded {
                Text("Ayrıntı metni")
            }
        }
        .opacity(isOn ? 1 : 0.4)
        .animation(.default, value: isOn)
    }
}

struct StatusText: View {
    var body: some View {
        Text("Hazır")
    }
}
```

Bu parça derlenmez ve çıktı vermez; yalnız kaynak olarak durur. Okuma düzeni şöyledir. `PanelScreen`, `isOn`'un sahibidir ve gövdesinde onu hem bağlama olarak hem değer olarak kullanır; yani değeri okur. `DetailPanel`, `isOn`'u parametre olarak alır ve saydamlığı için okur, `isExpanded`'ı ise kendi durumu olarak okur. `StatusText` hiçbirini okumaz. Bu okuma düzenini düz değerlerle yeniden kuralım. Model, çerçevenin bağımlılık izleme kuralını kurar; çerçevenin kendisi değildir.

İki değişiklik yazalım: panelin açık animasyonla açılması ve ekrandaki `isOn` değerinin değişmesi. İkincisinde örtük animasyon devrededir.

```swift
// main.swift — acik animasyonla sarilan yerel degisiklik ve ortuk animasyonun izledigi ebeveyn degeri
let storage: [String: Storage] = [
    "isOn": .state(owner: "PanelScreen"),
    "isExpanded": .state(owner: "DetailPanel"),
]

let screen = ViewTree(nodes: [
    ViewNode("PanelScreen", parent: nil, reads: ["isOn"]),
    ViewNode("DetailPanel", parent: "PanelScreen", reads: ["isOn", "isExpanded"]),
    ViewNode("StatusText", parent: "PanelScreen", reads: []),
], storage: storage)

let recomputes = [
    screen.change("panel-acildi", key: "isExpanded"),
    screen.change("etkin-degisti", key: "isOn"),
].flatMap { $0 }

countCauses(recomputes)
```

```
panel-acildi: isExpanded degisti | yeniden DetailPanel yerel | atlanan PanelScreen,StatusText
etkin-degisti: isOn degisti | yeniden PanelScreen yerel, DetailPanel ebeveyn | atlanan StatusText
yerel: 2
ebeveyn: 1
gozlem: 0
ortam: 0
```

İlk satır açık animasyonun ölçümüdür. `withAnimation` içindeki değişiklik, `isExpanded`'ın sahibi olan `DetailPanel`'i yeniden hesaplatır; tetik yereldir. Ekran ve durum metni atlanır. Animasyon bu sayıyı değiştirmedi, çünkü sarmalayıcı hangi görünümün okuduğunu belirlemez, yalnız yansımanın nasıl olacağını söyler.

İkinci satır örtük animasyonun ölçümüdür. `isOn` değişince sahibi `PanelScreen` yerel tetikle yeniden hesaplanır, `DetailPanel` ise değeri ebeveynden aldığı için ebeveyn tetikle. Örtük animasyonu taşıyan görünüm, değişikliği yapan görünüm değildir: değiştiriciyi `DetailPanel` taşır, değer ise ekrandadır. `StatusText` yine atlanır. Son sayım, iki yerel ve bir ebeveyn yeniden hesaplamayı verir; gözlem ve ortam sıfırdır.

**EK5.** Bu ölçüm yalnız `isExpanded`'ın `DetailPanel`'in kendi durumu ve `isOn`'un ekranın durumu olduğu ağaç için geçerlidir. Bu iki değerin aynı sahibe ait olduğu bir düzen ölçülmedi.

**EK6.** Bu ölçüm, `DetailPanel` gövdesinin `isOn`'u okuduğunu varsayar. Değiştiricinin izlediği değerin gövdede okunmadığı bir yazım ölçülmedi.

#### Animasyon Bir Tetik Değildir

Bu iki satır, dersin merkezindeki ayrımı gösterir. Açık animasyon bir değişikliği sarar, örtük animasyon bir değeri izler; ikisi de tetik üretmez. Tetik, hâlâ değerin kendi saklama yoludur: yerel durum, ebeveynden gelen değer, gözlemlenen nesne ya da ortam değeri. Animasyon bu dört kanalın hiçbirine beşinci bir kanal eklemez.

Bu ayrımın pratik sonucu hata ayıklarken ortaya çıkar. Bir görünümün beklenmedik biçimde yeniden hesaplandığı görülürse, animasyonu suçlamadan önce hangi değerin değiştiğine bakılır. Animasyonu kaldırmak yeniden hesaplamayı kaldırmaz; yalnız ara değerleri kaldırır. Öte yandan örtük animasyonun hangi değere bağlandığı önemlidir: `value:` parametresi, hangi değişikliğin animasyonla yansıyacağını seçer. Değer parametresi olmayan biçim, animasyonu görünümün altındaki her değişikliğe uygular ve kullanımdan kaldırılmıştır; bu yüzden animasyon, `value:` parametresiyle bir değere bağlanarak yazılır.

Açık animasyonun avantajı ise kapsamıdır. `withAnimation` bloğunun içinde yapılan değişiklikler o animasyonla yansır; bloğun dışında yapılan bir değişiklik bu animasyonu almaz, başka bir animasyon kaynağı yoksa animasyonsuz yansır. Aynı değer iki farklı yerden yazılıyorsa biri animasyonlu, öteki animasyonsuz olabilir. Model bu iki yazımı ayırmaz.

Bir kod incelemesinde bu ayrım üç soruya dönüşür. Birincisi, değer kimin: değeri tutan görünüm, değiştiren görünümle aynı mı? Aynıysa tetik yereldir ve animasyon ister açık ister örtük olsun yalnız o görünümü ilgilendirir. İkincisi, değeri kim okuyor: ebeveynden parametre ya da bağlama ile alan görünümler ebeveyn tetikle yeniden hesaplanır ve örtük animasyonu taşıyan görünüm çoğu zaman bunlardan biridir. Üçüncüsü, animasyon nerede yazılmış: değişikliği yapan kodda mı, değeri izleyen görünümde mi? Bu üçüncü soru ölçüm sonucunu değiştirmez, ama okuyanın niyeti bulmasını kolaylaştırır. Bir `withAnimation` bloğu, değişikliğin bilerek animasyonlandığını; bir `.animation(_:value:)` değiştiricisi ise görünümün bu değere her zaman animasyonla tepki vermesi istendiğini söyler.

Örnekteki `StatusText` iki satırda da atlandı. Bu, animasyonlu bir ekranın her görünümünün hareket ettiği anlamına gelmez; yalnız değeri okuyan görünüm yeniden hesaplanır. Okumayan görünüm, kardeşinin animasyonundan etkilenmeden olduğu gibi kalır.

**EK7.** Bu ölçüm animasyonun bir tetik üretmediğini, yalnız iki değişiklik için gösterir. Animasyonun bitiş anında çerçevenin bir ek hesaplama yapıp yapmadığı ölçülmedi.

**EK8.** Bu ölçüm süre, eğri ve kare sıklığı hakkında bir şey söylemez. Bu üç değer ne basıldı ne de karşılaştırıldı.

#### Sınırlayıcı Ölçüm

Aynı anahtarı iki kez yazalım; birinde değişiklik animasyonla sarılıdır, ötekinde değil. Model, bu farkı kuracak bir alan taşımaz. Bu bilinçli bir sınırdır: çekirdeğin `change` işlevi yalnız anahtarı ve etiketi alır.

```swift
// main.swift — animasyonlu ve animasyonsuz ayni degisiklik
let storage: [String: Storage] = [
    "isExpanded": .state(owner: "DetailPanel"),
]

let screen = ViewTree(nodes: [
    ViewNode("DetailPanel", parent: nil, reads: ["isExpanded"]),
    ViewNode("StatusText", parent: nil, reads: []),
], storage: storage)

let animated = screen.change("animasyonlu", key: "isExpanded")
let plain = screen.change("animasyonsuz", key: "isExpanded")
print(animated.map(\.view) == plain.map(\.view))
print(animated.map(\.cause) == plain.map(\.cause))
```

```
animasyonlu: isExpanded degisti | yeniden DetailPanel yerel | atlanan StatusText
animasyonsuz: isExpanded degisti | yeniden DetailPanel yerel | atlanan StatusText
true
true
```

İki değişiklik aynı satırı verir ve iki karşılaştırma da doğrudur: aynı görünüm, aynı tetik. Model animasyonu sayamaz; sayması da gerekmez, çünkü bu kursun ölçüsü yeniden hesaplamanın nedenidir, görünümün ara değerleri değil. Animasyonun ekranda nasıl göründüğü, ne kadar sürdüğü ve ara karelerde gövdenin yeniden çağrılıp çağrılmadığı ölçülmedi.

#### Özet

- Açık animasyon değişikliği, örtük animasyon görünümü sarar; ikisi de değeri değiştirmez, yansımanın biçimini seçer.
- Açık animasyonla sarılan yerel değişiklik, değerin sahibini yerel tetikle yeniden hesaplatır.
- Örtük animasyonun izlediği değer ebeveynden geliyorsa, o görünüm ebeveyn tetikle yeniden hesaplanır.
- Animasyonlu ve animasyonsuz aynı değişiklik modelde aynı görünümleri aynı tetikle sayar.
- Bu dersin tetiği: **yerel**.

#### Sonraki Adım

Bu derste değişiklik hep bir görünümü yeniden hesaplattı; görünüm ağaçta kaldı. Sonraki derste bir görünümün ağaca eklenmesini ve ağaçtan çıkarılmasını, yani geçişi ölçeceğiz. Orada bir modelden silinen öğenin hangi görünümü yeniden hesaplattığına, kalkan görünümün ise neden sayılmadığına bakacağız.

### Geçişler

Source: https://academia.sh/tr/kurslar/swiftui/gecisler

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ü.

Ö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.

```swift
// 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.

```swift
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.

```swift
// 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`.

```swift
// 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.

```swift
// 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.

### Erişilebilirlik

Source: https://academia.sh/tr/kurslar/swiftui/erisilebilirlik

Dinamik yazı boyutu ve hareket azaltma tercihinin ortam tetiğiyle, ebeveynin verdiği etiket metninin ebeveyn tetiğiyle yeniden hesaplamaya yol açtığının ölçümü.

Önceki derste silinen öğenin liste görünümünü gözlem tetiğiyle yeniden hesaplattığını gördük. Bu derste değişiklik uygulamanın içinden gelmez; kullanıcının sistem ayarlarından gelir. Soru yine aynıdır: **bu yeniden hesaplamayı ne tetikledi?** Bu derste bakılan tetik **ortam**.

Erişilebilirlik, arayüzün farklı ihtiyaçları olan kullanıcılar tarafından kullanılabilmesidir. Çerçeve bu bilgiyi üç yoldan taşır. Birincisi etiket: bir görünümün yardımcı teknolojiye ne diye tanıtılacağını `.accessibilityLabel(_:)` söyler. İkincisi erişilebilirlik özelliği (trait): bir görünümün düğme, başlık ya da görsel gibi hangi rolde olduğunu `.accessibilityAddTraits(_:)` bildirir. Üçüncüsü sistem tercihi: kullanıcı yazı boyutunu büyütmüş, hareketi azaltmış ya da renk şemasını değiştirmiş olabilir ve bu tercihler ortam değeri olarak görünümlere ulaşır. Dinamik yazı boyutu `@Environment(\.dynamicTypeSize)` ile, hareket azaltma tercihi `@Environment(\.accessibilityReduceMotion)` ile okunur. İlk iki yol görünümün kendi bildirimidir; üçüncüsü ise görünümün dışından gelen bir değerdir. Tetik açısından asıl fark da buradadır.

Ölçüm çekirdek dosyayla yapılır; çekirdek her derste aynı dosya adıyla ve aynı içerikle gelir.

```swift
// 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)")
    }
}
```

#### Üç Yol, Üç Okuma

Aşağıdaki parçada bir makale ekranı vardır. Başlık, yazı boyutunu okur. Özet kartı, ekranın verdiği bir metni etiket olarak kullanır ve hareket azaltma tercihine göre açılışını animasyonlu ya da animasyonsuz yapar. Kapatma düğmesinin etiketi ise sabit bir dizedir.

```swift
import SwiftUI

struct ArticleScreen: View {
    @State private var summary = "Özet"

    var body: some View {
        VStack {
            ArticleHeader()
            SummaryCard(label: summary)
            CloseButton()
        }
    }
}

struct ArticleHeader: View {
    @Environment(\.dynamicTypeSize) private var typeSize

    var body: some View {
        Text("Makale")
            .font(typeSize.isAccessibilitySize ? .title : .headline)
            .accessibilityAddTraits(.isHeader)
    }
}

struct SummaryCard: View {
    let label: String
    @Environment(\.accessibilityReduceMotion) private var reduceMotion
    @State private var isOpen = false

    var body: some View {
        Text("Özet")
            .accessibilityLabel(label)
            .onTapGesture {
                if reduceMotion {
                    isOpen.toggle()
                } else {
                    withAnimation {
                        isOpen.toggle()
                    }
                }
            }
    }
}

struct CloseButton: View {
    var body: some View {
        Button("Kapat") {}
            .accessibilityLabel("Makaleyi kapat")
    }
}
```

Bu parça derlenmez ve çıktı vermez; yalnız kaynak olarak durur. Okuma düzeni şöyledir. `ArticleScreen`, `summary`'nin sahibidir ve gövdesinde onu kartın etiketine iletir; yani değeri okur. `ArticleHeader`, `dynamicTypeSize` ortam değerini okur. `SummaryCard`, `summary`'yi etiket parametresi olarak ve `accessibilityReduceMotion` ortam değerini kapanışında okur. `CloseButton` hiçbir değer okumaz: etiketi bir dize sabitidir. Model, çerçevenin bağımlılık izleme kuralını kurar; çerçevenin kendisi değildir.

Üç değişiklik yazalım: kullanıcı yazı boyutunu büyütüyor, hareket azaltma tercihini açıyor ve ekranın özet metni değişiyor. Ortam değerini değiştirmek bu derste bir değiştirici değil, sistem ayarıdır.

```swift
// main.swift — yazi boyutu, hareket azaltma ve etiket metni
let storage: [String: Storage] = [
    "summary": .state(owner: "ArticleScreen"),
    "dynamicTypeSize": .environment,
    "accessibilityReduceMotion": .environment,
]

let screen = ViewTree(nodes: [
    ViewNode("ArticleScreen", parent: nil, reads: ["summary"]),
    ViewNode("ArticleHeader", parent: "ArticleScreen", reads: ["dynamicTypeSize"]),
    ViewNode("SummaryCard", parent: "ArticleScreen", reads: ["summary", "accessibilityReduceMotion"]),
    ViewNode("CloseButton", parent: "ArticleScreen", reads: []),
], storage: storage)

let recomputes = [
    screen.change("yazi-buyudu", key: "dynamicTypeSize"),
    screen.change("hareket-azaltildi", key: "accessibilityReduceMotion"),
    screen.change("ozet-degisti", key: "summary"),
].flatMap { $0 }

countCauses(recomputes)
```

```
yazi-buyudu: dynamicTypeSize degisti | yeniden ArticleHeader ortam | atlanan ArticleScreen,SummaryCard,CloseButton
hareket-azaltildi: accessibilityReduceMotion degisti | yeniden SummaryCard ortam | atlanan ArticleScreen,ArticleHeader,CloseButton
ozet-degisti: summary degisti | yeniden ArticleScreen yerel, SummaryCard ebeveyn | atlanan ArticleHeader,CloseButton
yerel: 1
ebeveyn: 1
gozlem: 0
ortam: 2
```

Yazı boyutu değişince yalnız başlık yeniden hesaplanır ve tetik ortamdır: değer ne başlığın ne de ebeveynin durumudur, sistemin sağladığı bir ortam değeridir. Hareket azaltma tercihi aynı biçimde yalnız kartı yeniden hesaplatır. Üçüncü satırda etiket metni değişir: sahibi olan ekran yerel tetikle, metni parametre olarak okuyan kart ebeveyn tetikle yeniden hesaplanır. Kartın ortam değerini de okuması bu satırı etkilemez, çünkü tetiği değişen anahtar belirler.

Ortam tetiğinin bir özelliği, değişikliği hiçbir görünümün yapmamasıdır. Yazı boyutu için bir atama, bir bağlama ya da bir geri çağrı yoktur; değer, görünümün dışında değişir ve onu okuyan görünüm yeniden hesaplanır. Bu yüzden ortam değerini okumak bir bağımlılık bildirimidir: görünümün gövdesi o değere bağlanmış olur.

Ortam değerinin ölçüsü ders boyunca aynı kalır: değeri kim okuyorsa yalnız o yeniden hesaplanır. Ama erişilebilirlik bağlamında bu kuralın bir sonucu daha vardır. Yazı boyutu gibi geniş etkili bir değeri ağacın tepesindeki bir görünüm okursa, o görünümün altındaki her şey etkilenmez; yalnız okuyan görünüm yeniden hesaplanır. Altındakilerin yeniden hesaplanması ancak onların da aynı değeri okumasıyla ya da ebeveynden değişen bir parametre almasıyla olur. Yani bu modelde erişilebilirlik desteği eklemek, ekranın tamamını yeniden hesaplanır hâle getirmez; çerçevenin metni kendi içinde nasıl ölçeklediği ölçülmedi. Bu kuralın görünüm sayısı arttıkça nasıl ölçeklendiği bu derste ölçülmedi.

**EK13.** Bu ölçüm yalnız `ArticleHeader`'ın `dynamicTypeSize`'ı, `SummaryCard`'ın `accessibilityReduceMotion`'ı okuduğu ağaç için geçerlidir. Her iki değeri de okuyan tek bir görünüm ölçülmedi.

**EK14.** Bu ölçüm yazı boyutunun hangi kademelere ayrıldığını, başlığın ekranda ne kadar yer kapladığını ya da satır sayısını söylemez. Bu üç şey ölçülmedi.

#### Etiket Neyi Okur

Etiketin ne okuduğu, onu nasıl yazdığınıza bağlıdır. Kartın etiketi ekranın durumundan geldi; ekran metni değiştirince kart yeniden hesaplandı. Kapatma düğmesinin etiketi ise bir sabittir ve hiçbir değere bağlı değildir. Sabit etiketin yardımcı teknolojide düzgün okunması önemlidir, ama etiket hiçbir ortam ya da durum değerini okumadığı için yeniden hesaplamayı da tetiklemez.

Bu ayrım, erişilebilirliği ek iş saymaktan alıkoyar. Etiketler ve özellikler çoğunlukla sabit ya da modelden gelen metinlerdir ve ölçüm bunların ek bir yeniden hesaplama getirmediğini gösterir: yalnız etiketin değeri bir durumdan geliyorsa, o durum değişince görünüm yeniden hesaplanır. Sistem tercihleri ise ayrıdır. Bir ekranın yazı boyutuna ya da hareket tercihine tepki vermesi, o ortam değerini okuyan görünümlerle sınırlıdır ve geri kalan görünümler etkilenmez.

**EK15.** Bu ölçüm etiketi dize olarak veren bir kalıba dayanır. Etiketin yerelleştirilmiş bir anahtardan geldiği durum bir sonraki dersin konusudur ve burada ölçülmedi.

**EK16.** Bu ölçüm yardımcı teknolojinin etiketi nasıl okuduğunu, sırayı ya da özelliklerin okunuşunu kapsamaz. Bunlar ölçülmedi.

#### Sınırlayıcı Ölçüm

Sabit dizeyle verilen etiketin hiçbir değer okumadığını ayrı bir blokta gösterelim. Ağaçta yalnız başlık ve kapatma düğmesi vardır. Yazı boyutu değişir; bu değeri başlık okur, düğme okumaz. Ardından hareket azaltma tercihi değişir; bu ağaçta onu okuyan yoktur.

```swift
// main.swift — sabit etiket hicbir degeri okumaz
let storage: [String: Storage] = [
    "dynamicTypeSize": .environment,
    "accessibilityReduceMotion": .environment,
]

let screen = ViewTree(nodes: [
    ViewNode("ArticleHeader", parent: nil, reads: ["dynamicTypeSize"]),
    ViewNode("CloseButton", parent: nil, reads: []),
], storage: storage)

let recomputes = [
    screen.change("yazi-buyudu", key: "dynamicTypeSize"),
    screen.change("hareket-azaltildi", key: "accessibilityReduceMotion"),
].flatMap { $0 }

print(recomputes.count)
```

```
yazi-buyudu: dynamicTypeSize degisti | yeniden ArticleHeader ortam | atlanan CloseButton
hareket-azaltildi: accessibilityReduceMotion degisti | yeniden yok | atlanan ArticleHeader,CloseButton
1
```

Kapatma düğmesi iki satırda da `atlanan` listesindedir: etiketi sabit olduğu için ne yazı boyutuna ne de başka bir ortam değerine bağlıdır. İkinci satırda okuyan görünüm olmadığı için yeniden hesaplanan listesi `yok` yazar. Sondaki `1`, iki değişiklikten yalnız birinin bir yeniden hesaplama ürettiğini gösterir. Bu, sistem tercihinin değişmesinin her zaman bir görünümü etkilemediğini gösterir. Düğmenin yazı boyutuna göre nasıl büyüdüğü, çerçevenin metni ölçeklemesi ve düzenin buna nasıl uyum sağladığı ölçülmedi; model yalnız hangi görünümün değeri okuduğunu sayar.

#### Özet

- Erişilebilirlik bilgisi etiket, özellik ve sistem tercihi olarak üç yoldan taşınır.
- Dinamik yazı boyutu ve hareket azaltma tercihi ortam değerleridir; yalnız onları okuyan görünüm ortam tetiğiyle yeniden hesaplanır.
- Etiket ekranın durumundan geliyorsa, o durum değişince okuyan görünüm ebeveyn tetikle yeniden hesaplanır.
- Sabit dizeyle verilen etiket hiçbir değer okumaz ve yeniden hesaplamaya yol açmaz.
- Bu dersin tetiği: **ortam**.

#### Sonraki Adım

Sistem tercihlerinden biri henüz ele alınmadı: kullanıcının dili ve bölgesi. Sonraki derste yerel ayarın ve yazı yönünün ortam değeri olarak hangi görünümleri yeniden hesaplattığını, tarih biçimleyen bir modelin özelliğinin ise hangi kanaldan geldiğini ölçeceğiz.

### Yerelleştirme

Source: https://academia.sh/tr/kurslar/swiftui/yerellestirme

Yerel ayar ve yazı yönünün ortam tetiğiyle, modelin tarih özelliğinin gözlem tetiğiyle yeniden hesaplamaya yol açtığının ve biçimlemenin ölçülmediğinin gösterimi.

Önceki derste kullanıcının sistem tercihlerinin, onları okuyan görünümü ortam tetiğiyle yeniden hesaplattığını ölçtük. Kullanıcının dili ve bölgesi de aynı türden bir tercihtir. Soru yine aynıdır: **bu yeniden hesaplamayı ne tetikledi?** Bu derste bakılan tetik **ortam**; yanında bir gözlem örneği de vardır.

Yerelleştirme üç ayrı konuyu kapsar. Birincisi metin: arayüzdeki bir ifadenin kullanıcının diline çevrilmiş karşılığı gösterilir. İkincisi biçim: tarih, sayı ve para birimi kullanıcının bölgesinin alışkanlığına göre yazılır; aynı gün bir yerde gün-ay, başka bir yerde ay-gün sırasıyla görünür. Üçüncüsü yazı yönü: bazı diller sağdan sola yazılır ve arayüzün düzeni bu yöne göre aynalanır. Çerçeve ilk ikisi için yerel ayarı, üçüncüsü için yazı yönünü ortam değeri olarak sunar: `@Environment(\.locale)` ve `@Environment(\.layoutDirection)`. Bu ikisi, önceki dersteki yazı boyutu gibi, görünümün dışından gelen değerlerdir.

Ölçüm çekirdek dosyayla yapılır; çekirdek her derste aynı dosya adıyla ve aynı içerikle gelir.

```swift
// 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)")
    }
}
```

#### Yerel Ayar, Yazı Yönü ve Tarih

Aşağıdaki parça bir etkinlik ekranıdır. Etkinliğin başlangıç tarihi, gözlemlenebilir bir nesnede durur. Başlık yerel ayarı okur. Tarih etiketi hem tarihi hem yerel ayarı okur, çünkü tarihi o yerel ayara göre biçimler. Eylem satırı yazı yönünü okur ve bir ok işaretinin yönünü ona göre seçer. Alt not ise ham metindir; yerelleştirme ona uygulanmaz.

```swift
import SwiftUI

@Observable
final class EventStore {
    var startDate = Date()
    var title = ""
}

struct EventScreen: View {
    let store: EventStore

    var body: some View {
        VStack {
            EventHeader()
            DateLabel(store: store)
            ActionRow()
            FooterNote()
        }
    }
}

struct EventHeader: View {
    @Environment(\.locale) private var locale

    var body: some View {
        Text(locale.identifier)
    }
}

struct DateLabel: View {
    let store: EventStore
    @Environment(\.locale) private var locale

    var body: some View {
        Text(store.startDate.formatted(.dateTime.day().month().locale(locale)))
    }
}

struct ActionRow: View {
    @Environment(\.layoutDirection) private var layoutDirection

    var body: some View {
        Text(layoutDirection == .rightToLeft ? "<" : ">")
    }
}

struct FooterNote: View {
    var body: some View {
        Text(verbatim: "v1")
    }
}
```

Bu parça derlenmez ve çıktı vermez; yalnız kaynak olarak durur. Okuma düzeni şöyledir. `EventScreen` doğrudan bir değer okumaz; yalnız çocukları kurar. `EventHeader` ve `DateLabel` yerel ayarı, `DateLabel` ayrıca `store.startDate`'i okur. `ActionRow` yazı yönünü okur. `FooterNote` hiçbir değer okumaz. Model, çerçevenin bağımlılık izleme kuralını kurar; çerçevenin kendisi değildir.

Üç değişiklik yazalım: kullanıcı dilini değiştiriyor, yazı yönü sağdan sola dönüyor ve etkinliğin tarihi değişiyor. Ortam değerlerini değiştirmek bu derste bir değiştirici değil, sistem ayarıdır.

```swift
// main.swift — yerel ayar, yazi yonu ve tarih
let storage: [String: Storage] = [
    "startDate": .model(object: "EventStore"),
    "locale": .environment,
    "layoutDirection": .environment,
]

let screen = ViewTree(nodes: [
    ViewNode("EventScreen", parent: nil, reads: []),
    ViewNode("EventHeader", parent: "EventScreen", reads: ["locale"]),
    ViewNode("DateLabel", parent: "EventScreen", reads: ["startDate", "locale"]),
    ViewNode("ActionRow", parent: "EventScreen", reads: ["layoutDirection"]),
    ViewNode("FooterNote", parent: "EventScreen", reads: []),
], storage: storage)

let recomputes = [
    screen.change("dil-degisti", key: "locale"),
    screen.change("yon-degisti", key: "layoutDirection"),
    screen.change("tarih-degisti", key: "startDate"),
].flatMap { $0 }

countCauses(recomputes)
```

```
dil-degisti: locale degisti | yeniden EventHeader ortam, DateLabel ortam | atlanan EventScreen,ActionRow,FooterNote
yon-degisti: layoutDirection degisti | yeniden ActionRow ortam | atlanan EventScreen,EventHeader,DateLabel,FooterNote
tarih-degisti: startDate degisti | yeniden DateLabel gozlem | atlanan EventScreen,EventHeader,ActionRow,FooterNote
yerel: 0
ebeveyn: 0
gozlem: 1
ortam: 3
```

Dil değişince yerel ayarı okuyan iki görünüm, başlık ve tarih etiketi, ortam tetiğiyle yeniden hesaplanır. Ekran, eylem satırı ve alt not atlanır. Yazı yönü değişince yalnız eylem satırı yeniden hesaplanır. Tarih değişince ise yalnız tarih etiketi hesaplanır ve tetik gözlemdir: tarih, bir model nesnesinin özelliğidir. Aynı etiket, hem ortamdan hem modelden değer okuduğu için iki ayrı kanaldan yeniden hesaplanabilir; hangi kanalın çalıştığını, değişen anahtar belirler.

Son sayım üç ortam ve bir gözlem yeniden hesaplamasını verir. Yerel ve ebeveyn sayıları sıfırdır, çünkü bu ağaçta hiçbir görünüm bir üst görünümden değişen bir değer almaz ve bir yerel durum değişmez. Ortam sayısının ağırlığı, yerelleştirmenin sistem tercihlerine dayanmasından gelir.

**EK17.** Bu ölçüm yalnız `DateLabel`'ın yerel ayarı açıkça `@Environment` ile okuduğu ağaç için geçerlidir. Metin sabitlerinin çerçeve tarafından yerel ayara göre çözümlenmesi ayrı bir iştir ve ağaçta okuma olarak yazılmadı.

**EK18.** Bu ölçüm yerel ayarın değiştiği anı tek bir etiketle temsil eder. Dil değişince uygulamanın yeniden başlatılıp başlatılmadığı ölçülmedi.

#### Yazı Yönü Neyi Değiştirir

Sağdan sola yazılan bir dilde arayüzün düzeni aynalanır: satırın başı sağda olur, ilerleme ve geri okları yön değiştirir. Çerçeve bunu yığıtlar ve listeler gibi kapsayıcılarla kendisi yapar; ama ok işareti gibi yöne bağlı içerik görünümün kendi kararıdır ve bu karar için `layoutDirection` ortam değeri okunur. Yön değişince bu değeri okuyan görünüm yeniden hesaplanır.

Burada iki tür yön bağımlılığı karışmamalıdır. Çerçevenin aynalamasını kendisi uyguladığı kısım, görünümün bir değer okumasını gerektirmez. Görünümün kendi içeriğini yöne göre seçmesi ise bir okuma ve dolayısıyla bir bağımlılıktır. Ölçüm yalnız ikincisini sayar. İlkinin ağaçta hangi görünümleri hesaplattığı çerçevenin iç işidir ve ölçülmedi.

**EK19.** Bu ölçüm yazı yönünün iki değerinden yalnız birinin değişimini temsil eder. Aynalanan düzenin ölçüleri, hizalama ve görünüm sırası ölçülmedi.

**EK20.** Bu ölçüm tarih etiketinin ekranda hangi biçimde yazıldığını söylemez. Tarihin yazılışı ölçülmedi.

#### Sınırlayıcı Ölçüm

Biçimleme, bir değerin ve bir yerel ayarın girdi olduğu bir işlevdir: bu derste kurulan işlevde aynı değer ve aynı yerel ayar her zaman aynı metni verir. Gerçek takvim biçimlemesi yerel ayarın yanında takvime ve saat dilimine de bağlıdır; bunlar ölçülmedi. Bu işlev bir yeniden hesaplamanın nedeni değildir; neden, işleve girdi olan yerel ayarın değişmesidir. Gerçek tarih ve sayı biçimleri yerel ayar verisine dayanır ve bu veri aygıta bağlıdır, bu yüzden bu derste ölçülmez. Aşağıdaki blok, biçimlemenin bir işlev olduğunu küçük bir örnekle gösterir ve ardından modelin yalnız yerel ayarı okuyan görünümü saydığını sınar.

```swift
// main.swift — bicimleme saf bir islevdir
func dayLabel(day: Int, month: Int, monthFirst: Bool) -> String {
    monthFirst ? "\(month)/\(day)" : "\(day).\(month)"
}

print(dayLabel(day: 5, month: 3, monthFirst: false))
print(dayLabel(day: 5, month: 3, monthFirst: true))

let storage: [String: Storage] = [
    "locale": .environment,
]

let screen = ViewTree(nodes: [
    ViewNode("DateLabel", parent: nil, reads: ["locale"]),
    ViewNode("FooterNote", parent: nil, reads: []),
], storage: storage)

let recomputes = screen.change("yerel-ayar-degisti", key: "locale")
print(recomputes.count)
```

```
5.3
3/5
yerel-ayar-degisti: locale degisti | yeniden DateLabel ortam | atlanan FooterNote
1
```

İlk iki satır aynı günün iki sırayla yazılışıdır; bu gerçek bir yerel ayar biçimi değil, yalnız bir saf işlev örneğidir. Gerçek bir takvim biçiminin çıktısı ölçülmedi. Üçüncü satır, modelin yalnız yerel ayarı okuyan görünümü saydığını gösterir: `DateLabel` ortam tetiğiyle yeniden hesaplanır, `FooterNote` atlanır. Biçimleme işlevinin kendisi bir görünüm değildir ve ağaçta yer almaz; son satırdaki `1`, yeniden hesaplanan görünüm sayısıdır.

#### Özet

- Yerelleştirme metin, biçim ve yazı yönünü kapsar; çerçeve bunlar için yerel ayarı ve yazı yönünü ortam değeri olarak sunar.
- Yerel ayar ya da yazı yönü değişince yalnız o değeri okuyan görünüm ortam tetiğiyle yeniden hesaplanır.
- Modelin tarih özelliği, onu okuyan görünümü gözlem tetiğiyle yeniden hesaplatır; aynı görünüm hem ortam hem gözlem kanalından hesaplanabilir.
- Biçimleme saf bir işlevdir; ağaçta yer almaz ve yalnız yerel ayarı okuyan görünüm sayılır.
- Bu dersin tetiği: **ortam**.

#### Sonraki Adım

Bu konunun son dersi, bu tetiklerin hızlı bir geri bildirim döngüsüyle nasıl görüldüğüne bakar. Ön izleme, bir görünümü örnek bir modelle çalıştırır; hangi örnek modelin verildiği, yukarıda ölçtüğümüz değerlerden hangilerinin ön izlemede değişebildiğini belirler. Sonraki derste ön izlemenin örnek model enjeksiyonunu ve yinelemeyi ele alacağız.

### Ön İzleme ve Yineleme

Source: https://academia.sh/tr/kurslar/swiftui/on-izleme-ve-yineleme

Ön izlemenin örnek modeli ve ezilen ortamıyla bir görünümü nasıl yeniden hesaplattığının, ön izlemenin açılışının ise neden değişiklik sayılmadığının gösterimi ve kursun yirmi bir derslik özeti.

Önceki derste yerel ayarın ve yazı yönünün ortam tetiğiyle yeniden hesaplamaya yol açtığını ölçtük. Bu derste hızlı geri bildirim döngüsüne bakıyoruz: bir görünümü uygulamanın tamamını çalıştırmadan, tek başına ekranda görmek. Soru yine aynıdır: **bu yeniden hesaplamayı ne tetikledi?** Bu derste bakılan tetik **gözlem**.

Ön izleme, bir görünümü kendi başına kurar. Görünüm bir modele ihtiyaç duyuyorsa ön izleme ona gerçek veri yerine bir örnek model verir. Görünümün okuduğu ortam değerlerini de ön izleme belirleyebilir; örneğin koyu renk şemasını zorlayabilir. Bu iki yol, daha önce ölçtüğümüz iki tetiğe denk gelir: örnek modelin özelliği gözlem, ezilen ortam değeri ortam tetiğidir. Ön izlemenin kendisi bir geliştirme aracının işidir ve bu derste ölçülmez; ölçülen, ön izlemenin görünüme verdiği değerlerdir.

Ölçüm çekirdek dosyayla yapılır; çekirdek her derste aynı dosya adıyla ve aynı içerikle gelir.

```swift
// 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)")
    }
}
```

#### Örnek Model ve Ezilen Ortam

Aşağıdaki parça bir profil ekranıdır. Ad ve durum, gözlemlenen bir modelin özellikleridir. Avatar renk şemasını ortamdan okur. İki ön izleme bildirimi aynı ekranı farklı biçimde kurar: biri modele kısa bir ad yazar, öteki renk şemasını koyuya çevirir.

```swift
import SwiftUI

@Observable
final class ProfileStore {
    var displayName = ""
    var status = ""
}

struct ProfileScreen: View {
    let store: ProfileStore

    var body: some View {
        VStack {
            AvatarView()
            NameLabel(store: store)
            StatusBadge(store: store)
        }
    }
}

struct AvatarView: View {
    @Environment(\.colorScheme) private var colorScheme

    var body: some View {
        Circle().fill(colorScheme == .dark ? Color.white : Color.black)
    }
}

struct NameLabel: View {
    let store: ProfileStore

    var body: some View {
        Text(store.displayName)
    }
}

struct StatusBadge: View {
    let store: ProfileStore

    var body: some View {
        Text(store.status)
    }
}

#Preview("Short name") {
    let store = ProfileStore()
    store.displayName = "Ada"
    return ProfileScreen(store: store)
}

#Preview("Dark") {
    ProfileScreen(store: ProfileStore())
        .environment(\.colorScheme, .dark)
}
```

Bu parça derlenmez ve çıktı vermez; yalnız kaynak olarak durur. Okuma düzeni şöyledir. `ProfileScreen` doğrudan bir değer okumaz; modeli çocuklara iletir ama özelliklerine dokunmaz. `NameLabel` `displayName`'i, `StatusBadge` `status`'u, `AvatarView` ise `colorScheme`'i okur.

Üç değişiklik yazalım. İlk ikisi örnek modelin özellikleridir: ad ve durum. Üçüncüsü, ön izlemenin ezdiği renk şemasının değişmesidir.

```swift
// main.swift — ornek model ve ezilen ortam
let storage: [String: Storage] = [
    "displayName": .model(object: "ProfileStore"),
    "status": .model(object: "ProfileStore"),
    "colorScheme": .environment,
]

let preview = ViewTree(nodes: [
    ViewNode("ProfileScreen", parent: nil, reads: []),
    ViewNode("AvatarView", parent: "ProfileScreen", reads: ["colorScheme"]),
    ViewNode("NameLabel", parent: "ProfileScreen", reads: ["displayName"]),
    ViewNode("StatusBadge", parent: "ProfileScreen", reads: ["status"]),
], storage: storage)

let recomputes = [
    preview.change("ornek-ad-degisti", key: "displayName"),
    preview.change("ornek-durum-degisti", key: "status"),
    preview.change("ezilen-kip-degisti", key: "colorScheme"),
].flatMap { $0 }

countCauses(recomputes)
```

```
ornek-ad-degisti: displayName degisti | yeniden NameLabel gozlem | atlanan ProfileScreen,AvatarView,StatusBadge
ornek-durum-degisti: status degisti | yeniden StatusBadge gozlem | atlanan ProfileScreen,AvatarView,NameLabel
ezilen-kip-degisti: colorScheme degisti | yeniden AvatarView ortam | atlanan ProfileScreen,NameLabel,StatusBadge
yerel: 0
ebeveyn: 0
gozlem: 2
ortam: 1
```

Örnek modelin adı değişince yalnız `NameLabel` yeniden hesaplanır ve tetik gözlemdir. Durum değişince yalnız `StatusBadge` hesaplanır. Ezilen renk şeması değişince yalnız `AvatarView` hesaplanır ve tetik ortamdır. `ProfileScreen` üç değişiklikte de atlanır, çünkü modeli yalnız iletir, hiçbir özelliğini okumaz. Son sayım iki gözlem ve bir ortam yeniden hesaplamasını verir; yerel ve ebeveyn sayıları sıfırdır, çünkü ağaçta bir durum sahibi ya da değer ileten üst görünüm yoktur.

Bu dersin ana ölçümü budur: ön izleme görünüme gerçek bir kaynaktan değil, kendi örnek modelinden değer verir; bu değer değişince görünüm, çalışan uygulamadaki gibi bağımlılık kuralına uyar. Bu yüzden bir görünümün hangi özellikleri okuduğunu ön izlemede görmek, o görünümün hangi değişiklikte yeniden hesaplanacağını görmenin ucuz bir yoludur.

**EK21.** Bu ölçüm örnek modeli iki özellikli bir nesne olarak ele alır. Gerçek bir modelin koleksiyonları ve iç içe nesneleri ölçülmedi.

**EK22.** Bu ölçüm ön izlemenin renk şemasını ezmesini, tek bir ortam anahtarının değişimiyle temsil eder. Ön izlemenin aynı anda birden çok ortam değerini ezmesi ölçülmedi.

#### Yineleme ve Geri Bildirim

Yineleme, kodu değiştirip sonucu yeniden görmektir. Ön izleme bu döngüyü kısaltır, çünkü görünümü yalnız kendi örnek verisiyle kurar. Burada iki ayrı şey karışmamalıdır. Görünümün kaynağını değiştirmek, ön izlemenin yeniden kurulmasına yol açabilir. Örnek modelin bir özelliğini değiştirmek ise çalışan bir görünümün yeniden hesaplanmasına yol açar. İlki bir kod değişikliğidir ve modelin konusu değildir; ikincisi bu kursun boyunca ölçtüğümüz türdendir.

Bunun pratik sonucu şudur. Bir görünüm ön izlemede beklenmedik sıklıkta yeniden hesaplanıyorsa, nedenini şu dört sorudan biriyle ararız: değer görünümün yerel durumu mu, ebeveynin ilettiği bir değer mi, bir modelin özelliği mi, yoksa ortamdan gelen bir değer mi? Ön izleme bu soruyu yanıtlamaz, ama yanıtı denemek için hızlı bir ortam sağlar.

**EK23.** Bu ölçüm yineleme döngüsünün süresini hiçbir biçimde temsil etmez. Ön izlemenin ne kadar hızlı yenilendiği ölçülmedi.

**EK24.** Bu ölçüm ön izlemenin kaynaktan yeniden kurulmasını bir değişiklik olarak saymaz. Kaynak düzenlemelerinin ağaca etkisi ölçülmedi.

#### Sınırlayıcı Ölçüm

Ön izlemenin açılışı bir değişiklik değildir; yeni bir başlangıç durumudur. Her ön izleme ağacı baştan kurar. Model bunu yeni bir `ViewTree` olarak kurar ve yeniden hesaplama saymaz. Aşağıdaki blokta aynı görünümler için iki ön izleme kuruyoruz. Birinin açılışı hiçbir kayıt bırakmaz. Değişiklik yalnız birinde yapılır ve ötekini etkilemez.

```swift
// main.swift — onizleme yeni bir baslangic durumudur
let storage: [String: Storage] = [
    "displayName": .model(object: "ProfileStore"),
]

let nodes = [
    ViewNode("ProfileScreen", parent: nil, reads: []),
    ViewNode("NameLabel", parent: "ProfileScreen", reads: ["displayName"]),
]

let first = ViewTree(nodes: nodes, storage: storage)
let second = ViewTree(nodes: nodes, storage: storage)

let firstRun = first.change("ornek-ad-degisti", key: "displayName")
print(firstRun.count)
print(second.nodes.count)
```

```
ornek-ad-degisti: displayName degisti | yeniden NameLabel gozlem | atlanan ProfileScreen
1
2
```

Kurulumun kayıt bırakmaması kodun kendisinden okunur: `ViewTree` başlatıcısı yalnız düğümleri ve depolama tablosunu saklar, yeniden hesaplama ise yalnız `change` çağrısıyla üretilir. İlk satır, değişikliğin ilk ağaçta yalnız `NameLabel`'ı yeniden hesaplattığını, `ProfileScreen`'i atladığını söyler; ikinci satır bu yeniden hesaplamaların sayısı olan `1`'dir. Üçüncü satır ikinci ağacın baştan kurulmuş, iki düğümlü, değişmemiş bir ağaç olduğunu gösterir: bir ön izlemedeki değişiklik öteki ön izlemeye yansımaz.

#### Kurs Kapanışı

Kurs boyunca yirmi bir ders, her biri bir yeniden hesaplamanın nedenini ölçtü. Aşağıdaki tablo her dersin ölçtüğü değişikliği, dersin tetiğini ve sınırlayıcı ölçümün neyi ölçemediğini gösterir.

| Ders | Değişiklik | Tetik | Sınırlayıcı ölçüm |
|---|---|---|---|
| Bildirimsel Sözdizim | altı değişiklik | dördü (2/2/2/2) | saklanmayan değer tetik üretmez |
| Görünümler ve Değiştiriciler | yazı tipi değişti | ortam | kendi yazı tipini koyan çocuk ortamı okumaz |
| Yığıt Düzenleri | aralık değişti | ebeveyn | kardeş katman değeri okuyamaz |
| Düzen Sistemi | kompakt oldu | ebeveyn | önerilen genişlik ölçüm dışı |
| Izgara ve Geometri | sınıf değişti | ortam | okuyucunun boyutu ölçüm dışı |
| Listeler ve Formlar | başlık değişti | gözlem | tek satır öteki satırları atlatır |
| Görsel Bileşenler | renk şeması değişti | ortam | statik görsel değer okumaz |
| Görünüm Kurucular | başlık değişti | ebeveyn | kapanış ağaçta düğüm değildir |
| Yerel Durum | dokunuldu | yerel | kardeşin durumu okunamaz |
| Bağlama | çocuk yazdı | ebeveyn | kural yazanı okumaz |
| Gözlemlenebilir Nesneler | kitap eklendi | gözlem | nesne düzeyi bildirim daha geniştir |
| Ortam Değerleri | kip değişti | ortam | ezme tek kayıtla ölçülmez |
| Gezinme Yığıtı ve Yolu | ekran itildi | yerel | itilen hedef yeni düğümdür |
| Sunum ve Kapatma | sayfa açıldı | yerel | kapatma eylemi değer değildir |
| Sekme ve Araç Çubuğu | arşive geçildi | yerel | seçilmeyen sekme atlanır |
| Hareketler | sürükleme bitti | yerel | sürükleme hızı saklanmaz |
| Örtük ve Açık Animasyonlar | panel açıldı | yerel | animasyon farkı ölçülmez |
| Geçişler | ileti silindi | gözlem | kalkan görünüm ağaçta yoktur |
| Erişilebilirlik | yazı büyüdü | ortam | sabit etiket değer okumaz |
| Yerelleştirme | dil değişti | ortam | biçimleme saf işlevdir |
| Ön İzleme ve Yineleme | örnek ad değişti | gözlem | ön izleme yeni başlangıçtır |

Açılış dersi dışındaki yirmi dersin tetik dağılımı şöyledir: yerel 6, ebeveyn 4, gözlem 4, ortam 6. Açılış dersinin çekirdeği ayrıca 2/2/2/2 dağılımını verdi. İkisi aynı şey değildir: çekirdek sekiz yeniden hesaplama sayar, dağılım ise açılış dışındaki yirmi dersin tetiğini sayar.

#### Özet

- Ön izleme bir görünüme örnek bir model ve gerekirse ezilmiş ortam değerleri verir; ölçülen, bu değerlerdir.
- Örnek modelin bir özelliği değişince onu okuyan görünüm gözlem tetiğiyle yeniden hesaplanır.
- Ön izlemenin ezdiği ortam değeri değişince onu okuyan görünüm ortam tetiğiyle yeniden hesaplanır.
- Ön izlemenin açılışı bir değişiklik değil yeni bir başlangıç durumudur; ağaç baştan kurulur ve yeniden hesaplama sayılmaz.
- Yirmi bir dersin her biri bir tetiği ölçtü; sınırlayıcı ölçümler modelin neyi ölçemediğini gösterdi.
- Bu dersin tetiği: **gözlem**.

#### Sonraki Adım

Bu kurs, bildirimsel bir arayüzün neden yeniden hesaplandığını ölçmekle bitti. Sıradaki kurs konu alanını değiştirecek: buyurgan arayüz çerçevesiyle yazılmış bir ekranı kavrayacak ve ikisi arasında köprü kuracağız. Bu kursta öğrendiğimiz soru ve dört tetik, o köprünün iki yanını karşılaştırırken dayanacağımız dil olacak.

