---
title: 'UIKit ve Köprüleme'
source: 'https://academia.sh/tr/kurslar/uikit'
language: tr
updated: '2026-10-21T05:03:15+00:00'
license: 'CC BY-SA 4.0'
lessons: 15
outcomes:
    - 'Görünüm denetleyicisi yaşam döngüsünü yönetir'
    - 'Kısıt tabanlı düzenle ekran kurar'
    - 'İki arayüz çerçevesini aynı uygulamada birlikte kullanır'
    - 'Ekranda gösterilen bir alanın modelle uyumlu mu, bayat mı, eksik mi yoksa artık mı olduğunu ayırt eder'
    - 'Temsilci ve hedef-eylem bağlarını küçük bir modelle kurar ve modelin çerçevenin kendisi olmadığını tanır'
---

# UIKit ve Köprüleme

Buyurgan bir arayüzde model değişince ekranı kod günceller; güncellemeyi unutan kod çökmez, ekran modelden kayar. Kurs her düzeneği tek bir soruyla ölçer — bu ekran modelle uyuşuyor mu? Gösterilen değer modelle aynı mı, eski mi, hiç yazılmamış mı, yoksa önceki bir değerden mi kalmış? Görünüm denetleyicileri ve yaşam döngüsü, gezinme ve sunum, bağlantılar ve eylemler, kısıt tabanlı düzen, yığıt, tablo ve koleksiyon görünümleri, temsilci kalıbı, çizim ve katman animasyonu ile iki arayüz çerçevesinin aynı uygulamada birlikte kullanımı aynı çerçeveden geçirilir.

## Görünüm Denetleyicileri

Ekran başına sorumluluk taşıyan görünüm denetleyicisi, yükleme, görünme ve kaybolma aşamalarındaki yaşam döngüsü yöntemleri, yığıt tabanlı gezinme denetleyicileri, kip ve itmeli sunum biçimleri, görsel düzenleyiciyle ve kodla arayüz kurmanın karşılaştırması, arayüz ile kod arasındaki bağlantılar ve eylemler.

### Görünüm Denetleyicisi Kavramı

Source: https://academia.sh/tr/kurslar/uikit/gorunum-denetleyicisi-kavrami

Bir görünüm denetleyicisinin ekranı model ile görünüm arasında nasıl taşıdığının ve bir ekranın modelle uyuşup uyuşmadığını uyumlu, bayat, eksik ve artık olarak sınıflayan ölçümün tanıtımı.

Önceki bir kursta (M30/K03) bildirimsel çerçeve, bir görünümün neden yeniden hesaplandığını bir tetikle okuttu; değer değişince çerçeve ekranı kendisi güncelledi. Bu kurs o işi programcıya geri verir. Buyurgan çerçevede model değişir ve ekrandaki her etiketi, her satırı güncellemek yazılan koda kalır. Güncellemeyi yazmayı unutmak hata vermez; ekran yalnızca eski şeyi göstermeye devam eder.

Bu yüzden kursun sorusu bir tetik değil, bir sonuçtur: **bu ekran modelle uyuşuyor mu**. Kod koştuktan sonra model ne diyor, ekran ne gösteriyor, ikisi aynı mı. Neden değil, sonuç ölçüdür.

#### Görünüm Denetleyicisi Ne Yapar

Buyurgan çerçevede ekranın tamamı bir **görünüm denetleyicisi** (view controller) nesnesine bağlıdır. Denetleyici üç iş yapar. Modeli okuyup görünümlere yazar. Kullanıcıdan gelen olayı, örneğin bir düğmeye basılmasını, modele taşır. Bir de ekranın ne zaman açıldığını ve kapandığını bildiren çağrıları alır. Model ile görünüm arasında başka bir köprü yoktur: denetleyici bir değeri ekrana yazmadıysa ekran onu bilmez.

Aşağıdaki parça bir gelen kutusu ekranının iskeletidir. Platform çerçevesini içe aldığı için bu derste çalıştırılmaz.

```swift
import UIKit

final class InboxController: UIViewController, FolderPickerDelegate {
    let store: MailStore
    private let titleLabel = UILabel()
    private let unreadLabel = UILabel()

    init(store: MailStore) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        titleLabel.text = store.values["title"] ?? ""
        unreadLabel.text = store.values["unread"] ?? ""
    }

    func markAllRead() {
        store.update("unread", "0")
    }

    func folderPicker(_ picker: FolderPicker, didChoose folder: String) {
        titleLabel.text = folder
    }
}
```

Üç yöntem üç ayrı yolu gösterir. `viewDidLoad` modeli ekrana yazar. `markAllRead` yalnızca modeli değiştirir ve ekrana dokunmaz. `folderPicker(_:didChoose:)` ekrana yazar ama modeli değiştirmez; modeli klasör seçici kendisi günceller. İlk bakışta üçü de makul görünür. Hangisinin ekranı modelle eşit bıraktığını okuyarak değil, ölçerek öğreneceğiz.

#### Görünümün Sahibi Olmak

Her görünüm denetleyicisinin bir `view` özelliği vardır ve bu özellik tembel yüklenir: görünüm, ilk kez istendiğinde oluşturulur. Görünümü kodla kurmak isteyen denetleyici bunu `loadView` içinde yapar; görünümü dosyadan alan denetleyicide çerçeve bu işi kendisi görür. Her iki durumda da görünüm yüklendikten sonra çağrılan ilk yöntem `viewDidLoad` olur ve alt görünümlerin eklenmesi için olağan yer burasıdır.

```swift
import UIKit

final class ProfileController: UIViewController {
    private let nameLabel = UILabel()

    override func loadView() {
        view = UIView()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        nameLabel.frame = CGRect(x: 16, y: 80, width: 240, height: 24)
        view.addSubview(nameLabel)
    }
}
```

Bu parçada `loadView` yalnızca kök görünümü atar; etiketin eklenmesi ve konumlanması `viewDidLoad` içindedir. `loadView` içinde `super.loadView()` çağrılmaz, çünkü kök görünümü atayan yöntem odur. Görünüm henüz yüklenmemişken `view` özelliğine dokunan bir çağrı yüklemeyi başlatır; bu, yanlış sırada yazılmış kodda şaşırtıcı sonuçlara yol açar.

#### Sık Yapılan Hatalar

Buyurgan çerçevede en sık görülen hatalar, ekrana yazma çağrısının bir yerde eksik kalmasından doğar ve çoğu hata vermeden çalışır:

- **Modeli değiştirip ekranı yazmamak.** Düğmeye basılınca model güncellenir, etiket aynı kalır. Ekran bayat kalır ve hiçbir hata görünmez.
- **Yeni alanı yalnızca bir yere yazmak.** Modele bir alan eklenir, `viewDidLoad` ekrana yazar, ama alanı değiştiren yer ekranı yenilemez. İlk açılışta doğru görünen ekran sonraki değişikliklerde geride kalır.
- **Silme çağrısını unutmak.** Hücre yeniden kullanılırken ya da klasör değişirken eski içerik temizlenmez; ekranda modelin artık taşımadığı bir değer kalır.
- **Temsilciyi bağlamamak.** Zayıf tutulan temsilci `nil` ise çağrı sessizce atlanır.
- **Temsilciyi güçlü tutmak.** Denetleyici ile yardımcı nesne birbirini güçlü tutar ve ikisi de serbest kalmaz.

Bu listenin her maddesi aynı biçimdedir: kod çalışır, hata vermez, ekran yanlış kalır. Bu yüzden kurs çağrının kendisini değil sonucunu, yani ekranın modelle uyuşmasını ölçer.

#### Ölçü: Dört Sonuç

Ölçüm bir ekranın gösterdiği her alanı modelin o anki değeriyle karşılaştırır ve dört sonuçtan birini verir.

- **uyumlu:** model ve ekran aynı değeri taşıyor.
- **bayat:** ikisi de bir değer taşıyor ama farklı; ekran eski kalmış.
- **eksik:** model bir değer taşıyor, ekran hiç göstermiyor.
- **artık:** ekran bir değer gösteriyor, model o alanı taşımıyor.

İki tarafta da olmayan alan `olcum-disi` yazılır ve hiçbir sayıma girmez. Çekirdek ayrı bir dosyadır ve kurs boyunca her derste aynen kullanılır.

```swift
// Sync.swift — ekranda gosterilen her alani modelin o anki degeriyle karsilastirir
final class Screen {
    let name: String
    private(set) var shown: [String: String] = [:]

    init(_ name: String) { self.name = name }

    func set(_ key: String, _ value: String) { shown[key] = value }
    func clear(_ key: String) { shown[key] = nil }
}

func verdict(model: String?, shown: String?) -> String {
    switch (model, shown) {
    case (nil, nil): return "olcum-disi"
    case (nil, _?): return "artik"
    case (_?, nil): return "eksik"
    case let (wanted?, displayed?): return wanted == displayed ? "uyumlu" : "bayat"
    }
}

func check(_ label: String, model: [String: String], screen: Screen, keys: [String]) -> [String] {
    keys.map { key in
        let result = verdict(model: model[key], shown: screen.shown[key])
        print("\(label) | \(screen.name).\(key): model=\(model[key] ?? "-") ekran=\(screen.shown[key] ?? "-") -> \(result)")
        return result
    }
}

func countVerdicts(_ verdicts: [String]) {
    for name in ["uyumlu", "bayat", "eksik", "artik"] {
        print("\(name): \(verdicts.filter { $0 == name }.count)")
    }
}
```

`Screen`, bir denetleyicinin ekranda gösterdiği alanların kaydıdır; `set` bir alanı yazar, `clear` siler. `verdict` kuralın kendisidir ve neden bakmaz: yalnızca iki değere bakar. `check` bir olaydan sonra verilen anahtarları denetler ve her satırı basar, `countVerdicts` dört sonucu sayar.

#### Beş Olay

Model, denetleyicinin ve çevresindeki nesnelerin saf Swift karşılığıdır. Aşağıdaki kod çerçeveyi çalıştırmaz; yukarıdaki parçanın tip ve yöntem adlarını taşıyan, ama çağrıları `main.swift`'in yaptığı bir kopyadır. Başka bir deyişle model, kodun ekrana ne yazdığını kurar; çerçevenin kendisi değildir. Klasör seçici bir **temsilci** (delegate) protokolüyle denetleyiciye haber verir ve temsilci referansı **zayıf** tutulur, çünkü denetleyici seçiciyi, seçici denetleyiciyi güçlü tutarsa iki nesne birbirini serbest bırakamaz: bu bir **tutma döngüsüdür** (retain cycle).

Beş olay sırayla koşar: ekran yüklenir, tüm iletiler okundu işaretlenir, klasör değişir, modele iki yeni alan gelir ve bir hücre yeni içerikle yeniden kullanılır.

```swift
// main.swift — bes olay, dokuz alan, dort uyum
final class MailStore {
    private(set) var values: [String: String] = [:]
    func update(_ key: String, _ value: String) { values[key] = value }
}

protocol FolderPickerDelegate: AnyObject {
    func folderPicker(_ picker: FolderPicker, didChoose folder: String)
}

final class FolderPicker {
    weak var delegate: FolderPickerDelegate?
    let store: MailStore

    init(store: MailStore) { self.store = store }

    func choose(_ folder: String) {
        store.update("title", folder)
        delegate?.folderPicker(self, didChoose: folder)
    }
}

final class InboxController: FolderPickerDelegate {
    let store: MailStore
    let screen = Screen("InboxController")

    init(store: MailStore) { self.store = store }

    func viewDidLoad() {
        screen.set("title", store.values["title"] ?? "")
        screen.set("unread", store.values["unread"] ?? "")
    }

    func markAllRead() {
        store.update("unread", "0")
    }

    func folderPicker(_ picker: FolderPicker, didChoose folder: String) {
        screen.set("title", folder)
    }
}

final class MessageCell {
    let screen = Screen("MessageCell")

    func configure(_ message: [String: String]) {
        for key in message.keys.sorted() {
            screen.set(key, message[key] ?? "")
        }
    }
}

let store = MailStore()
store.update("title", "Gelen")
store.update("unread", "3")
let inbox = InboxController(store: store)
inbox.viewDidLoad()
let loaded = check("yuklendi", model: store.values, screen: inbox.screen, keys: ["title", "unread"])

inbox.markAllRead()
let marked = check("okundu-isaretlendi", model: store.values, screen: inbox.screen, keys: ["unread"])

let picker = FolderPicker(store: store)
picker.choose("Arsiv")
let moved = check("klasor-degisti", model: store.values, screen: inbox.screen, keys: ["title"])

store.update("drafts", "2")
store.update("flagged", "1")
let drafted = check("taslak-geldi", model: store.values, screen: inbox.screen, keys: ["drafts", "flagged"])

let cell = MessageCell()
cell.configure(["subject": "Fatura", "badge": "yeni", "attachment": "pdf"])
let next = ["subject": "Toplanti"]
cell.configure(next)
let reused = check("hucre-yeniden-kullanildi", model: next, screen: cell.screen, keys: ["subject", "badge", "attachment"])

countVerdicts(loaded + marked + moved + drafted + reused)
```

```
yuklendi | InboxController.title: model=Gelen ekran=Gelen -> uyumlu
yuklendi | InboxController.unread: model=3 ekran=3 -> uyumlu
okundu-isaretlendi | InboxController.unread: model=0 ekran=3 -> bayat
klasor-degisti | InboxController.title: model=Arsiv ekran=Gelen -> bayat
taslak-geldi | InboxController.drafts: model=2 ekran=- -> eksik
taslak-geldi | InboxController.flagged: model=1 ekran=- -> eksik
hucre-yeniden-kullanildi | MessageCell.subject: model=Toplanti ekran=Toplanti -> uyumlu
hucre-yeniden-kullanildi | MessageCell.badge: model=- ekran=yeni -> artik
hucre-yeniden-kullanildi | MessageCell.attachment: model=- ekran=pdf -> artik
uyumlu: 3
bayat: 2
eksik: 2
artik: 2
```

Ölçüm dokuz alanı denetledi: 3 uyumlu, 2 bayat, 2 eksik, 2 artık. Olay sayısı (beş) ile alan sayısı (dokuz) aynı şey değildir; hücrenin yeniden kullanıldığı olay tek başına üç alanı denetler.

Satırları sırayla okuyalım. `yuklendi` iki alanı da uyumlu bıraktı, çünkü `viewDidLoad` iki alanı da yazıyor. `okundu-isaretlendi` bayat: **hedef-eylem** (target-action) bağı modeli değiştirdi ama etiketi güncellemedi; model 0, ekran hâlâ 3 diyor. `klasor-degisti` de bayat, ve nedeni daha gizli: temsilci hiç bağlanmadığı için `weak var delegate` `nil` kaldı, `delegate?.folderPicker` çağrısı sessizce hiçbir şey yapmadı. Model Arsiv oldu, ekran Gelen kaldı. `taslak-geldi` iki eksik: modele gelen `drafts` ve `flagged` alanlarını ekran hiç yazmadı. `hucre-yeniden-kullanildi` bir uyumlu, iki artık: hücre yeni iletinin konusunu yazdı ama önceki iletiden kalan rozeti ve eki silmedi.

Dört sonucun hepsi aynı kökten çıkıyor: ekrana yazmak bir çağrıdır ve çağrı yoksa yazma yoktur. Bayat, çağrının hiç yapılmadığı ya da hedefine ulaşmadığı yerde; eksik, çağrının hiç yazılmadığı yerde; artık, silme çağrısının yazılmadığı yerde ortaya çıkar. Bu dağılımın kendisi bir bulgu değildir. Beş olayı biz seçtik; çerçevede bu sonuçların hangisinin ne sıklıkta çıkacağını bu ölçüm söylemez.

**GD1.** Bu dersin modelinde ekrana yazan çağrılar ile modele yazan çağrılar ayrı tutulur ve model her zaman doğru kabul edilir. Denetleyicinin ekrana yazdığı değeri gerçek bir etiketin sonradan farklı çizip çizmediği ölçülmedi.

#### Ölçümü Okumak

Yukarıdaki sayılar bir denetim listesi değil, o beş olayın sonucudur. Bir ekranda hangi sonucun ne kadar çıkacağı, olayların seçimine bağlıdır. Ölçümün gösterdiği şey, dört sonucun da tek bir gerçekçi denetleyicide yan yana bulunabildiğidir; hangisinin daha sık olduğunu söylemez. Bayat ve eksik, ekranın yazılmadığı yerde; artık, silinmediği yerde ortaya çıkar. Uyumlu satırlar ise ekranın doğru olduğunu söyleyen tek kanıttır ve bu yüzden onları da saymak gerekir.

Bir satırın sonucu okunurken sorulacak soru "kod ne yaptı" değil, "ekranda ne kaldı" olmalıdır. Kodun doğru göründüğü bir yerde ekranın yanlış kalması mümkündür; ekranın doğru olduğu yerde kodun yanlış görünmesi de. Ölçüm yalnız ekranda kalana bakar.

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

Kural değerleri karakter karakter karşılaştırır. Bu iki sınırı birlikte gösterir: biçimlenmiş metin eşit sayılmaz ve iki tarafta da olmayan alan sayılmaz.

```swift
let label = "3 ileti"
let screen = Screen("InboxController")
screen.set("unread", label)
let result = check("bicimli-metin", model: ["unread": "3"], screen: screen, keys: ["unread"])
countVerdicts(result)
screen.clear("unread")
let cleared = check("temizlendi", model: [:], screen: screen, keys: ["unread"])
countVerdicts(cleared)
```

```
bicimli-metin | InboxController.unread: model=3 ekran=3 ileti -> bayat
uyumlu: 0
bayat: 1
eksik: 0
artik: 0
temizlendi | InboxController.unread: model=- ekran=- -> olcum-disi
uyumlu: 0
bayat: 0
eksik: 0
artik: 0
```

Model `3` diyor, ekran `3 ileti` gösteriyor. İnsan gözüyle ikisi uyuşuyor, ama kural bunu bayat sayar; çünkü kural neden bakmaz ve biçimlemeyi bilmez. Ekranı `clear` ile boşaltınca ve modelde de alan bulunmayınca satır `olcum-disi` basar ve dört sayı da sıfır kalır. Alan ne modelde ne ekranda olduğundan ortada karşılaştırılacak bir şey yoktur.

Bu iki sınır sonraki derslerde de geçerlidir. Biçimlenmiş bir etiketi ölçecek ders, ya biçimlenmiş metni modele de yazar ya da karşılaştırmayı biçimlenmemiş değer üzerinden yapar ve bunu açıkça söyler.

#### Özet

- Görünüm denetleyicisi modeli ekrana, ekrandaki olayları modele taşıyan nesnedir; ekrana yazılmayan değeri ekran bilmez.
- Ölçü ekseni "bu ekran modelle uyuşuyor mu"dur ve dört sonuç verir: uyumlu, bayat, eksik, artık.
- Beş olayda dokuz alan denetlendi: 3 uyumlu, 2 bayat, 2 eksik, 2 artık.
- Bağlanmamış zayıf temsilci hata vermez; çağrı sessizce yapılmaz ve ekran bayat kalır.
- Kural biçimlenmiş metni eşit saymaz ve iki tarafta da olmayan alanı saymaz.
- Bu dersin uyumu: **dördü** (3/2/2/2).

#### Sonraki Adım

Bu derste ekranı yazan çağrı `viewDidLoad` idi ve onu ekranın bir kez çalışan kurulumu gibi okuduk. Gerçekte denetleyici yüklenir, görünür, gizlenir ve yeniden görünür; her aşamanın kendi çağrısı vardır. Sıradaki derste yaşam döngüsü yöntemlerinde bir değeri nereye yazdığımızın ekranı modelle nasıl ayırabildiğini ölçeceğiz.

### Yaşam Döngüsü Yöntemleri

Source: https://academia.sh/tr/kurslar/uikit/yasam-dongusu-yontemleri

Bir değeri yaşam döngüsü yöntemlerinin hangisinde yazmanın ekranı modelle uyuşur ya da bayat bıraktığının ve yazma yerini taşımanın sonucunun ölçümü.

Önceki derste ekranı yazan çağrı `viewDidLoad` idi ve onu ekranın kurulumu gibi okuduk. Bir denetleyicinin ömrü ise tek bir kurulumdan ibaret değildir: görünüm yüklenir, ekrana gelir, ekrandan kalkar ve aynı denetleyici yeniden ekrana gelebilir. Her aşamanın çağrıldığı bir **yaşam döngüsü yöntemi** (lifecycle method) vardır ve bir değeri hangisine yazdığımız, ekranın modelle ne zaman buluşacağını belirler.

Bu derste ölçülen şey sırayı değil, yazma yerinin sonucudur: aynı değer iki farklı yöntemde yazıldığında ekran modelle uyuşuyor mu.

#### Yöntemler ve Çağrı Anları

Aşağıdaki parça bir fatura ekranının iskeletidir. Platform çerçevesini içe aldığı için çalıştırılmaz.

```swift
import UIKit

final class InvoiceController: UIViewController {
    let store: InvoiceStore
    private let totalLabel = UILabel()
    private let statusLabel = UILabel()

    init(store: InvoiceStore) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        totalLabel.text = store.values["total"] ?? ""
        statusLabel.text = store.values["status"] ?? ""
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
    }
}
```

`viewDidLoad`, denetleyicinin görünümü belleğe yüklendikten sonra bir kez çağrılır. `viewWillAppear` ise ekrana gelmeden hemen önce her seferinde çağrılır; denetleyici gizlenip tekrar göründüğünde de çalışır. Bu iki yöntemin bu farkı dersin tamamını taşır. Çerçevenin çağrı sırasının geri kalanı, yani görünmeden sonraki ve kaybolmaya ilişkin yöntemler, bu derste ölçülmez; düzyazıda anılır.

İskeletteki denetleyici değerleri yalnız `viewDidLoad` içinde yazıyor. `viewWillAppear` yeniden tanımlanmış ama içinde ekrana yazan bir şey yok.

#### Yaşam Döngüsünün Tamamı

Önceki bölümde iki yöntemi gördük. Çerçevenin çağırdığı yöntemlerin sırası şöyledir: görünüm yüklenir (`viewDidLoad`), ekrana gelmeden önce `viewWillAppear`, geldikten sonra `viewDidAppear`, ekrandan kalkmadan önce `viewWillDisappear`, kalktıktan sonra `viewDidDisappear`. Bunların dışında yerleşim hesaplanırken çağrılan `viewWillLayoutSubviews` ve `viewDidLayoutSubviews` vardır.

```swift
import UIKit

final class InvoiceController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        // görünüm bir kez yüklendi: alt görünümleri kur
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // her gösterimden önce: modeldeki güncel değeri yaz
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // ekran göründü: bir süre kullanıcıya bağlı işleri başlat
    }

    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // ekrandan kalkmadan önce: kaydedilmemiş girdiyi modele yaz
    }
}
```

Her yöntemin başında `super` çağrısı vardır; yeniden tanımlanan yaşam döngüsü yöntemlerinde bu çağrı unutulursa üst sınıfın işi yapılmaz. `animated` parametresi geçişin animasyonlu olup olmadığını söyler. Aynı denetleyici gizlenip yeniden göründüğünde `viewDidLoad` tekrar çağrılmaz; `viewWillAppear` ve `viewDidAppear` ise her gösterimde çağrılır.

#### Ölçü

Ölçü, önceki dersin çekirdeğidir; aynen kullanılır.

```swift
// Sync.swift — ekranda gosterilen her alani modelin o anki degeriyle karsilastirir
final class Screen {
    let name: String
    private(set) var shown: [String: String] = [:]

    init(_ name: String) { self.name = name }

    func set(_ key: String, _ value: String) { shown[key] = value }
    func clear(_ key: String) { shown[key] = nil }
}

func verdict(model: String?, shown: String?) -> String {
    switch (model, shown) {
    case (nil, nil): return "olcum-disi"
    case (nil, _?): return "artik"
    case (_?, nil): return "eksik"
    case let (wanted?, displayed?): return wanted == displayed ? "uyumlu" : "bayat"
    }
}

func check(_ label: String, model: [String: String], screen: Screen, keys: [String]) -> [String] {
    keys.map { key in
        let result = verdict(model: model[key], shown: screen.shown[key])
        print("\(label) | \(screen.name).\(key): model=\(model[key] ?? "-") ekran=\(screen.shown[key] ?? "-") -> \(result)")
        return result
    }
}

func countVerdicts(_ verdicts: [String]) {
    for name in ["uyumlu", "bayat", "eksik", "artik"] {
        print("\(name): \(verdicts.filter { $0 == name }.count)")
    }
}
```

#### Değeri Nereye Yazmak

Model, yukarıdaki denetleyiciyi ve onun düzeltilmiş bir kopyasını saf Swift ile kurar. Bu kod çerçeveyi çalıştırmaz: model, kodun ekrana ne yazdığını kurar; çerçevenin kendisi değildir. `appear()` yöntemi dersin seçtiği bir sırayı uygular: görünüm yüklenmediyse önce `viewDidLoad`, ardından her seferinde `viewWillAppear`.

`InvoiceController` iskeletteki gibi yazar: iki alanı da yalnız `viewDidLoad` içinde. `FixedInvoiceController` aynı iki alanı `viewWillAppear` içinde yazar. Olay şudur: fatura ekranı açılır, kullanıcı başka bir ekrana geçer, orada tutar değişir ve kullanıcı geri döner. Dönüşte yalnız `viewWillAppear` çağrılır.

```swift
// main.swift — iki yazma yeri, uc olay
final class InvoiceStore {
    private(set) var values: [String: String] = [:]
    func update(_ key: String, _ value: String) { values[key] = value }
}

final class InvoiceController {
    let store: InvoiceStore
    let screen = Screen("InvoiceController")
    private var isViewLoaded = false

    init(store: InvoiceStore) { self.store = store }

    func appear() {
        if !isViewLoaded {
            isViewLoaded = true
            viewDidLoad()
        }
        viewWillAppear()
    }

    func viewDidLoad() {
        screen.set("total", store.values["total"] ?? "")
        screen.set("status", store.values["status"] ?? "")
    }

    func viewWillAppear() {}
}

final class FixedInvoiceController {
    let store: InvoiceStore
    let screen = Screen("FixedInvoiceController")
    private var isViewLoaded = false

    init(store: InvoiceStore) { self.store = store }

    func appear() {
        if !isViewLoaded {
            isViewLoaded = true
            viewDidLoad()
        }
        viewWillAppear()
    }

    func viewDidLoad() {}

    func viewWillAppear() {
        screen.set("total", store.values["total"] ?? "")
        screen.set("status", store.values["status"] ?? "")
    }
}

let store = InvoiceStore()
store.update("total", "120")
store.update("status", "odenmedi")

let loadOnly = InvoiceController(store: store)
loadOnly.appear()
let loadedOld = check("yuklendi-eski", model: store.values, screen: loadOnly.screen, keys: ["total", "status"])

let fixed = FixedInvoiceController(store: store)
fixed.appear()
let loadedFixed = check("yuklendi-duzeltilmis", model: store.values, screen: fixed.screen, keys: ["total", "status"])

store.update("total", "150")
loadOnly.appear()
fixed.appear()
let backOld = check("geri-donuldu-eski", model: store.values, screen: loadOnly.screen, keys: ["total", "status"])
let backFixed = check("geri-donuldu-duzeltilmis", model: store.values, screen: fixed.screen, keys: ["total", "status"])

countVerdicts(loadedOld + loadedFixed + backOld + backFixed)
```

```
yuklendi-eski | InvoiceController.total: model=120 ekran=120 -> uyumlu
yuklendi-eski | InvoiceController.status: model=odenmedi ekran=odenmedi -> uyumlu
yuklendi-duzeltilmis | FixedInvoiceController.total: model=120 ekran=120 -> uyumlu
yuklendi-duzeltilmis | FixedInvoiceController.status: model=odenmedi ekran=odenmedi -> uyumlu
geri-donuldu-eski | InvoiceController.total: model=150 ekran=120 -> bayat
geri-donuldu-eski | InvoiceController.status: model=odenmedi ekran=odenmedi -> uyumlu
geri-donuldu-duzeltilmis | FixedInvoiceController.total: model=150 ekran=150 -> uyumlu
geri-donuldu-duzeltilmis | FixedInvoiceController.status: model=odenmedi ekran=odenmedi -> uyumlu
uyumlu: 7
bayat: 1
eksik: 0
artik: 0
```

İlk yüklemede iki denetleyici de uyumlu: ikisi de ilk görünmede değerleri yazıyor. Fark dönüşte çıkıyor. Tutar değiştiğinde `viewDidLoad` bir daha çağrılmaz; `InvoiceController`in ekranı 120'de kalır, model 150 der ve satır bayat çıkar. `status` alanı hiç değişmediği için aynı denetleyicide uyumlu kalır. Aynı alan, aynı olay, tek fark yazma yeridir: `FixedInvoiceController` her görünmede yazdığı için 150'yi gösterir.

Yazım yeri bir tercihtir. `viewDidLoad` bir kez yapılacak işin yeridir: alt görünümleri kurmak, hedefleri bağlamak. Modelden okunup ekrana yazılan ve zamanla değişebilen bir değerin yeri, her görünmede çağrılan yöntemdir. Ekran açıkken değişen bir model değeri ise görünmeyle yakalanamaz; bu ders onu ölçmez.

**GD1.** Modelde `appear()` görünümü yalnız ilk çağrıda yükler ve her çağrıda `viewWillAppear` çalıştırır. Bu, çerçevenin davranışının dersin kurduğu özetidir; çerçevenin kendi çağrı sırası, hangi yöntemin önce çağrıldığı ya da çağrıların sayısı ölçülmedi.

#### Sık Yapılan Hatalar

- **Güncel değeri yalnızca `viewDidLoad` içinde yazmak.** Denetleyici bir kez yüklenir, ama başka bir ekrandan dönüldüğünde model değişmiş olabilir. Dönüşte ekran eski değeri gösterir; bu dersin ölçtüğü şey budur.
- **Her gösterimde tekrar etmemesi gereken işi `viewWillAppear` içine koymak.** Kurulum işi, örneğin alt görünüm eklemek ya da bir kaydı açmak, her gösterimde yeniden koşar. Bir kaydı `viewWillAppear` içinde açan denetleyici, onu `viewWillDisappear` içinde kapatmalıdır.
- **`super` çağrısını atlamak.** Üst sınıf bu yöntemlerde kendi işini yapar; çağrı yoksa iş de yoktur.
- **Görünüm boyutuna `viewDidLoad` içinde güvenmek.** O anda görünümün sınırları henüz nihai değildir. Boyuta bağlı bir hesap yerleşim yöntemlerine aittir.
- **Uzun bir işi `viewWillAppear` içinde beklemek.** Ekranın görünmesi o yöntemin dönmesine bağlıdır.

İlk madde dersin ölçümüdür: aynı değer iki farklı yöntemde yazılabilir ve yalnız biri ekranı modelle uyumlu tutar.

#### Ölçümü Okumak

İki denetleyici aynı depoyu okuyor, aynı alanları yazıyor ve aynı olaylarla karşılaşıyor. Aralarındaki tek fark yazma yeridir. Ölçümün sekiz satırının yedisi uyumlu, biri bayattır; bayat satır, yalnız `viewDidLoad` içinde yazan denetleyicinin ekrana geri dönüldükten sonraki halidir. Düzeltilmiş denetleyici aynı olayda uyumludur.

Bu sonuç bir yönteme "doğru yer" denmesinin gerekçesi olarak okunmamalıdır. Değer bir kez okunup değişmeyecekse `viewDidLoad` yeterlidir ve her gösterimde yeniden yazmak gereksiz tekrar olur. Ölçüm yalnız şunu söyler: model gösterimler arasında değişiyorsa ve yazma yeri yalnız yüklemeyse ekran bayat kalır. Hangi alanın gösterimler arasında değişebildiğini bilmek, yazma yerini seçmenin ön koşuludur.

#### Yerleşim Yöntemleri ve Yazma Yeri Seçimi

`viewDidLayoutSubviews` görünümlerin yerleşimi hesaplandığında çağrılır ve bu bir kez olmaz: ekran döndüğünde, boyut değiştiğinde ya da alt görünümlerin ölçüsü değiştiğinde yeniden çağrılır. Bu yöntemin işi, nihai ölçülere bakıp bir hesap yapmaktır; alt görünüm eklemek ya da bir kaydı açmak gibi tek seferlik işler buraya konmaz, çünkü her çağrıda tekrarlanırlar.

Bir değeri nereye yazacağını seçmek için üç soru yeterlidir:

- Değer bir kez okunup ekran boyunca sabit mi kalıyor? O zaman `viewDidLoad` içinde yazılır.
- Değer, ekran başka bir ekranın arkasında kalıp geri geldiğinde değişmiş olabilir mi? O zaman `viewWillAppear` içinde yazılır; böylece ekran görünmeden önce güncel olur.
- Değer, ekran görünürken kullanıcının bir eylemiyle mi değişiyor? O zaman yazma, o eylemin kendi içinde yapılır; yaşam döngüsü yöntemleri bu değişimi yakalamaz.

Üçüncü durum, bu kursun bağlantılar ve eylemler dersine kalır. Şimdilik önemli olan, ilk ikisinin ayrımıdır: ikisi de ekranı kurar, ama biri yalnız ilk yüklemede, öteki her gösterimde çalışır.

Çocuk denetleyiciler de bu çağrıları alır: bir denetleyici başka birinin içine yerleştirildiğinde yaşam döngüsü çağrıları ona da iletilir, ve `isMovingToParent` o anda denetleyicinin yeni eklendiğini söyler. Bu dersteki ölçüm tek bir denetleyiciyle sınırlı kalır; çocuk denetleyici ilişkisi ölçülmez.

#### Çağrı Sırasında Dikkat Edilecekler

Her yaşam döngüsü yönteminin üst sınıf sürümü çağrılmalıdır. `super.viewWillAppear(animated)` satırı atlanırsa üst sınıfın yaptığı iş yarım kalır ve bu hata çoğu kez yalnız belirli ekranlarda, belirli bir sıradan sonra görünür. Çağrının yeri de önemlidir: kuruluş işlerinde üst sınıf çağrısı başa, kapanış işlerinde sona konması yaygın bir alışkanlıktır.

Ağır işler bu yöntemlerin içinde yapılmamalıdır. `viewWillAppear` ekranın görünmesini bekletir; içinde uzun bir okuma varsa geçiş takılır. Uzun işler başka bir yere alınır, sonuç gelince ekran yazılır. Sonuç geldiğinde ekran artık görünmüyor olabilir; bu durumda yazma çağrısı yine de güvenlidir, çünkü görünüm ağacı denetleyici yaşadığı sürece durur, ama emek boşa gider.

Son olarak, bu yöntemleri elle çağırmak yanlıştır. Çağıran sistemdir; denetleyicinin kendi `viewDidLoad` yöntemini kodla çağırması, görünümün iki kez kurulmasına yol açar. Ekranı yeniden yazmak istendiğinde, yazma işini ayrı bir yönteme koyup onu çağırmak doğrudur.

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

Model çağrı sırasını `main.swift` verir; çerçeve vermez. Aşağıdaki blok bunu görünür kılar: aynı denetleyici, çağrı kaydıyla, ilk görünmeden önce modeli değiştirilmiş hâlde.

```swift
// main.swift — cagri sirasi dersin secimidir
final class InvoiceStore {
    private(set) var values: [String: String] = [:]
    func update(_ key: String, _ value: String) { values[key] = value }
}

final class LoggedInvoiceController {
    let store: InvoiceStore
    let screen = Screen("LoggedInvoiceController")
    private(set) var calls: [String] = []
    private var isViewLoaded = false

    init(store: InvoiceStore) { self.store = store }

    func appear() {
        if !isViewLoaded {
            isViewLoaded = true
            viewDidLoad()
        }
        viewWillAppear()
    }

    func viewDidLoad() {
        calls.append("viewDidLoad")
        screen.set("total", store.values["total"] ?? "")
    }

    func viewWillAppear() {
        calls.append("viewWillAppear")
    }
}

let store = InvoiceStore()
store.update("total", "120")
let controller = LoggedInvoiceController(store: store)
store.update("total", "130")
controller.appear()
controller.appear()
print(controller.calls.joined(separator: ", "))
let early = check("gorunmeden-once", model: store.values, screen: controller.screen, keys: ["total"])
countVerdicts(early)
```

```
viewDidLoad, viewWillAppear, viewWillAppear
gorunmeden-once | LoggedInvoiceController.total: model=130 ekran=130 -> uyumlu
uyumlu: 1
bayat: 0
eksik: 0
artik: 0
```

Burada model, denetleyici kurulduktan sonra ama görünmeden önce değişti ve `viewDidLoad` o anki değeri okudu; satır uyumlu. Yazılan çağrı kaydı bir ölçüm değildir: o sırayı `appear()` yazdı. Gerçek çerçevenin sırası bu bloktan okunmaz, iskelet parçadan ve düzyazıdan okunur.

#### Özet

- Yaşam döngüsü yöntemleri denetleyicinin ömrünün aşamalarına bağlı çağrılardır; `viewDidLoad` bir kez, `viewWillAppear` her görünmede çağrılır.
- Zamanla değişen bir değeri yalnız `viewDidLoad` içinde yazmak, ekran başka bir ekranın altında kalırken model değişince ekranı bayat bırakır.
- Aynı yazmayı her görünmede çağrılan yönteme taşımak, aynı olayda ekranı modelle uyumlu tutar.
- Hiç değişmeyen alan her iki yerde de uyumlu kalır; fark yalnız değişen alanda çıkar.
- Modeldeki çağrı sırası dersin seçimidir; çerçevenin gerçek sırası ölçülmez.
- Bu dersin uyumu: **bayat**.

#### Sonraki Adım

Bu derste ekrana geri dönüş tek bir olaydı. Dönüşü asıl üreten şey, bir ekranın başka bir ekranın üstüne konup sonra çıkarılmasıdır. Sıradaki derste gezinme denetleyicilerini kuracağız ve bir ekran yığıttan çıkarıldığında modelden kalkan bir seçimin ekranda kalıp kalmadığını ölçeceğiz.

### Gezinme Denetleyicileri

Source: https://academia.sh/tr/kurslar/uikit/gezinme-denetleyicileri

Bir gezinme denetleyicisinde ekran itilip geri çıkarıldığında modelden kalkan bir seçimin listede artık olarak kaldığının ve itilen ekranın modelle uyuştuğunun ölçümü.

Önceki derste ekran bir kez ekrana geldi, başka bir ekranın altında kaldı ve geri döndü. O başka ekran nereden geliyordu? Çoğu uygulamada ekranlar birbirinin üstüne **gezinme denetleyicisi** (navigation controller) ile konur. Gezinme denetleyicisi ekranları bir **yığıtta** tutar: yeni ekran yığıtın üstüne **itilir** (push), geri dönülünce üstteki ekran yığıttan **geri çıkarılır** (pop) ve altındaki ekran yeniden görünür.

Bu iş bir yönetim işidir; ekranların içeriğini denetleyici yönetmez. Altta kalan ekranın ne gösterdiği, o ekranın denetleyicisinin yazdığı şeydir. Bu derste bunun bir sonucunu ölçeriz: liste ekranında seçili bir satır var, kullanıcı ayrıntıya iner ve geri döner; seçim modelden kalkmıştır ama liste bundan haberdar mıdır.

#### İtme ve Geri Çıkarma

Aşağıdaki parça bir kişi listesini ve ayrıntı ekranını gösterir. Platform çerçevesini içe aldığı için çalıştırılmaz.

```swift
import UIKit

final class ContactListController: UIViewController {
    let store: ContactStore
    private let selectedLabel = UILabel()
    private let countLabel = UILabel()

    init(store: ContactStore) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        countLabel.text = store.values["count"] ?? ""
    }

    func select(_ name: String) {
        store.update("selected", name)
        selectedLabel.text = name
        navigationController?.pushViewController(ContactDetailController(store: store), animated: true)
    }
}

final class ContactDetailController: UIViewController {
    let store: ContactStore
    private let selectedLabel = UILabel()
    private let phoneLabel = UILabel()

    init(store: ContactStore) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        selectedLabel.text = store.values["selected"] ?? ""
        phoneLabel.text = store.values["phone"] ?? ""
    }

    override func didMove(toParent parent: UIViewController?) {
        super.didMove(toParent: parent)
        if parent == nil { store.remove("selected") }
    }
}
```

`select` seçimi hem modele hem listenin etiketine yazar ve ayrıntıyı iter. Ayrıntı denetleyicisi yığıttan çıkarıldığında `didMove(toParent:)` ebeveyni `nil` olarak alır ve seçimi modelden kaldırır. Listenin kendi denetleyicisinde seçim etiketini temizleyen bir kod yoktur.

#### Yığıtı Yöneten Denetleyici

Gezinme denetleyicisi, çocuk denetleyicilerden oluşan bir yığıt tutar. En üstteki denetleyici ekranda görünür; `pushViewController` yığıtın üstüne bir denetleyici koyar, `popViewController` en üsttekini çıkarır. Yığıt, içindeki denetleyicileri güçlü tutar; bu yüzden yığıtta bekleyen bir denetleyici bellekte kalır ve görünümü yeniden yüklenmez.

```swift
import UIKit

final class ContactListController: UIViewController {
    let store: ContactStore

    init(store: ContactStore) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    func showDetail(for name: String) {
        let detail = ContactDetailController(store: store, name: name)
        navigationController?.pushViewController(detail, animated: true)
    }
}
```

`navigationController` özelliği, denetleyicinin içinde bulunduğu gezinme denetleyicisini verir; denetleyici bir yığıtta değilse `nil` döner ve itme çağrısı sessizce hiçbir şey yapmaz. Gezinme çubuğundaki geri düğmesi çerçevenin kendisi tarafından bağlanır: dokunuş en üstteki denetleyiciyi geri çıkarır ve `popViewController` çağrısını kodla yazmak gerekmez.

#### Ölçü

Ölçü, kurs boyunca aynı çekirdektir.

```swift
// Sync.swift — ekranda gosterilen her alani modelin o anki degeriyle karsilastirir
final class Screen {
    let name: String
    private(set) var shown: [String: String] = [:]

    init(_ name: String) { self.name = name }

    func set(_ key: String, _ value: String) { shown[key] = value }
    func clear(_ key: String) { shown[key] = nil }
}

func verdict(model: String?, shown: String?) -> String {
    switch (model, shown) {
    case (nil, nil): return "olcum-disi"
    case (nil, _?): return "artik"
    case (_?, nil): return "eksik"
    case let (wanted?, displayed?): return wanted == displayed ? "uyumlu" : "bayat"
    }
}

func check(_ label: String, model: [String: String], screen: Screen, keys: [String]) -> [String] {
    keys.map { key in
        let result = verdict(model: model[key], shown: screen.shown[key])
        print("\(label) | \(screen.name).\(key): model=\(model[key] ?? "-") ekran=\(screen.shown[key] ?? "-") -> \(result)")
        return result
    }
}

func countVerdicts(_ verdicts: [String]) {
    for name in ["uyumlu", "bayat", "eksik", "artik"] {
        print("\(name): \(verdicts.filter { $0 == name }.count)")
    }
}
```

#### Geri Dönülünce Liste

Model, iskeletteki iki denetleyiciyi ve yığıtı saf Swift ile kurar. Model, kodun ekrana ne yazdığını kurar; çerçevenin kendisi değildir. Gezinme denetleyicisi yalnız bir ad listesi tutar, çünkü ölçülen şey yığıtın kendisi değil, yığıt değişince ekranların ne yazdığıdır. Geri çıkarma ile `didMove(toParent:)` çağrısının sırası dersin seçimidir.

Olaylar sırayla şunlardır: liste yüklenir ve bir kişi seçilir, ayrıntı itilir ve yüklenir, ayrıntı geri çıkarılır.

```swift
// main.swift — liste, detay, geri cikarma
final class ContactStore {
    private(set) var values: [String: String] = [:]
    func update(_ key: String, _ value: String) { values[key] = value }
    func remove(_ key: String) { values[key] = nil }
}

final class ContactNavigation {
    private(set) var stack: [String] = []
    func push(_ name: String) { stack.append(name) }
    func pop() { _ = stack.popLast() }
}

final class ContactListController {
    let store: ContactStore
    let screen = Screen("ContactListController")

    init(store: ContactStore) { self.store = store }

    func viewDidLoad() {
        screen.set("count", store.values["count"] ?? "")
    }

    func select(_ name: String) {
        store.update("selected", name)
        screen.set("selected", name)
    }
}

final class ContactDetailController {
    let store: ContactStore
    let screen = Screen("ContactDetailController")

    init(store: ContactStore) { self.store = store }

    func viewDidLoad() {
        screen.set("selected", store.values["selected"] ?? "")
        screen.set("phone", store.values["phone"] ?? "")
    }

    func didMove(toParent parent: ContactNavigation?) {
        if parent == nil { store.remove("selected") }
    }
}

let store = ContactStore()
store.update("count", "2")
store.update("phone", "555-0100")
let navigation = ContactNavigation()
let list = ContactListController(store: store)
navigation.push("ContactListController")
list.viewDidLoad()
list.select("Ayse")
let chosen = check("secildi", model: store.values, screen: list.screen, keys: ["selected", "count"])

let detail = ContactDetailController(store: store)
navigation.push("ContactDetailController")
detail.viewDidLoad()
let pushed = check("detay-itildi", model: store.values, screen: detail.screen, keys: ["selected", "phone"])

navigation.pop()
detail.didMove(toParent: nil)
let popped = check("geri-cikarildi", model: store.values, screen: list.screen, keys: ["selected", "count"])

countVerdicts(chosen + pushed + popped)
```

```
secildi | ContactListController.selected: model=Ayse ekran=Ayse -> uyumlu
secildi | ContactListController.count: model=2 ekran=2 -> uyumlu
detay-itildi | ContactDetailController.selected: model=Ayse ekran=Ayse -> uyumlu
detay-itildi | ContactDetailController.phone: model=555-0100 ekran=555-0100 -> uyumlu
geri-cikarildi | ContactListController.selected: model=- ekran=Ayse -> artik
geri-cikarildi | ContactListController.count: model=2 ekran=2 -> uyumlu
uyumlu: 5
bayat: 0
eksik: 0
artik: 1
```

Seçim ve ayrıntı ilk iki olayda uyumlu. Üçüncü olayda ayrıntı denetleyicisi modeli doğru güncelledi: seçim modelden kalktı. Ama listenin etiketi hâlâ Ayse diyor. Satır `artik` çıktı: ekran bir değer gösteriyor, model o alanı taşımıyor. `count` alanı hiç değişmediği için aynı listede uyumlu kaldı. Sorun ayrıntı denetleyicisinin yanlış yazmasında değil, listenin hiçbir şey yazmamasındadır; modeli değiştirmek ekranı değiştirmez.

Bu sonuç, ayrıntı ekranı yığıttan çıktığında listenin yeniden görünmesine rağmen oluşur. Liste yeniden göründüğünde önceki derste gördüğümüz `viewWillAppear` çağrılır, ama listede o yöntemi yeniden tanımlayan ve seçimi modelle eşitleyen bir kod yoktur. Çözüm, o yöntemde seçim etiketini modelden yeniden yazmak ya da model boşsa temizlemektir. Bu dersin ölçümü çözümü ölçmez; yalnız sorunu gösterir.

**GD1.** Modelde seçimi kaldıran çağrı `didMove(toParent:)` içinde, geri çıkarmadan sonra gelir. Gerçek çerçevedeki bu iki olayın sırası ölçülmedi; sonuç sıraya bağlı değildir, çünkü liste bu iki çağrıdan hiçbirinde yazmaz.

#### Sık Yapılan Hatalar

- **Listenin yeniden yüklendiğini varsaymak.** Detaydan geri dönüldüğünde liste denetleyicisi yığıttaki yerinde bekliyordu; `viewDidLoad` yeniden çağrılmaz. Değer yalnız orada yazıldıysa ekran eski kalır.
- **Geri çıkarmayı `viewWillDisappear` ile yakalamak.** Bu yöntem, üstüne yeni bir denetleyici itildiğinde de çağrılır. Denetleyicinin gerçekten yığıttan çıktığını anlamak için `isMovingFromParent` ya da `didMove(toParent:)` kullanılır.
- **Aynı denetleyici örneğini iki kez itmek.** Çerçeve bunu desteklemez ve çalışma anında hata verir. Her itme için yeni bir örnek yaratılır.
- **Yığıtta olmayan bir denetleyiciden itmek.** `navigationController` `nil` ise itme sessizce yapılmaz.
- **Detayın bıraktığı durumu listede temizlemeyi unutmak.** Detay ekranı seçili kişiyi işaretler ve listeden dönülürken işaret silinmezse listede artık bir değer kalır. Bu dersin ölçtüğü sonuç budur.

#### Ölçümü Okumak

Altı satırın beşi uyumlu, biri artıktır. Artık satırı, geri çıkarmadan sonra liste ekranında kalan `selected` alanıdır: model o alanı artık taşımıyor, ekran hâlâ gösteriyor. Bu bir yazma eksikliği değil, silme eksikliğidir; liste denetleyicisi yığıtta yaşamaya devam ettiği için temizlik çağrısı kimseye düşmedi.

Dikkat çeken yan, bu hatanın detay ekranından kaynaklanmamasıdır. Detay denetleyicisi kendi işini yaptı ve `didMove(toParent:)` içinde modelden değeri kaldırdı. Ekranın modelle uyuşması, modelin değiştiği her ekranın kendi yazma çağrısına bağlıdır; model değişince yığıttaki diğer denetleyicilerin haberi olmaz. Liste ekranı yığıtta beklerken değişen modeli ancak yeniden göründüğünde okuyabilir, ve okumayı kimse yazmadıysa okumaz.

#### Başlık, Çubuk Düğmeleri ve Yığıtı Yeniden Kurmak

Her denetleyici, gezinme çubuğunda görünecekleri `navigationItem` üzerinden tanımlar: `title` çubuğun başlığını, `rightBarButtonItem` sağdaki düğmeyi verir. Geri düğmesinin başlığı, bir önceki denetleyicinin başlığından gelir. Bu ayarlar `viewDidLoad` içinde yapılır; denetleyici yığıtta kaldığı sürece bir kez yazılmaları yeter.

```swift
import UIKit

final class ContactDetailController: UIViewController {
    let store: ContactStore
    let name: String

    init(store: ContactStore, name: String) {
        self.store = store
        self.name = name
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = name
    }

    override func didMove(toParent parent: UIViewController?) {
        super.didMove(toParent: parent)
        if parent == nil {
            store.remove("selected")
        }
    }
}
```

Yığıt, tek bir itme ve geri çıkarmadan fazlasını da yapabilir. `popToRootViewController` kök denetleyiciye kadar hepsini çıkarır; `viewControllers` dizisi yığıtın tamamını verir; `setViewControllers` yığıtı bütünüyle yeniden kurar. Bunların her biri, çıkan denetleyicilerin yaşam döngüsü çağrılarını alması anlamına gelir. Birden çok denetleyiciyi birden çıkaran bir çağrıdan sonra, hepsinin bıraktığı durumun temizlendiğinden emin olmak gerekir.

İki ekranın aynı veriyi göstermesi için çoğu kez ortak bir model nesnesi verilir: liste ve detay aynı depoya bakar. Bu, ekranların aynı kaynaktan okuyacağını garanti eder, ama okuma çağrısının ne zaman yapılacağını garanti etmez. Dersin ölçümü bu boşluğu gösterir.

Bir de şu ayrıntı var: `pushViewController` çağrısı yığıta ekler, ama geçişin görsel olarak bitmesini beklemez. Çağrıdan hemen sonra yazılan kod, yeni denetleyicinin henüz görünmediği bir anda çalışır. Bu yüzden yeni ekranın içeriğine bağlı işler, yeni denetleyicinin kendi yaşam döngüsü yöntemlerine bırakılır.

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

Bu ölçümün iki sınırı vardır. Birincisi: itilen ayrıntı yeni bir `Screen`dir ve henüz bir şey yazmamıştır. Çekirdek bu ekranı `viewDidLoad` öncesinde denetlerse model bir değer taşıdığı için `eksik` der. Bu bir sapma değil, ekranın henüz kurulmamış olmasıdır. İkincisi: geri çıkarılan ekran yığıttan ayrılır ve artık denetlenmez; ölçüm yalnız görünür ekranlara bakar.

```swift
// main.swift — itilen ekran ve cikarilan ekran
final class ContactStore {
    private(set) var values: [String: String] = [:]
    func update(_ key: String, _ value: String) { values[key] = value }
}

final class ContactNavigation {
    private(set) var stack: [String] = []
    func push(_ name: String) { stack.append(name) }
    func pop() { _ = stack.popLast() }
}

final class ContactDetailController {
    let store: ContactStore
    let screen = Screen("ContactDetailController")

    init(store: ContactStore) { self.store = store }

    func viewDidLoad() {
        screen.set("selected", store.values["selected"] ?? "")
        screen.set("phone", store.values["phone"] ?? "")
    }
}

let store = ContactStore()
store.update("selected", "Ayse")
store.update("phone", "555-0100")
let navigation = ContactNavigation()
navigation.push("ContactListController")
let detail = ContactDetailController(store: store)
navigation.push("ContactDetailController")
let before = check("itildi-yazmadan-once", model: store.values, screen: detail.screen, keys: ["selected", "phone"])
detail.viewDidLoad()
let after = check("itildi-yazdiktan-sonra", model: store.values, screen: detail.screen, keys: ["selected", "phone"])
navigation.pop()
print(navigation.stack.joined(separator: ", "))
countVerdicts(before + after)
```

```
itildi-yazmadan-once | ContactDetailController.selected: model=Ayse ekran=- -> eksik
itildi-yazmadan-once | ContactDetailController.phone: model=555-0100 ekran=- -> eksik
itildi-yazdiktan-sonra | ContactDetailController.selected: model=Ayse ekran=Ayse -> uyumlu
itildi-yazdiktan-sonra | ContactDetailController.phone: model=555-0100 ekran=555-0100 -> uyumlu
ContactListController
uyumlu: 2
bayat: 0
eksik: 2
artik: 0
```

Kural ekranın ne zaman kurulmuş sayılacağını bilmez. İtilen ekran yazmadan önce `eksik`, yazdıktan sonra `uyumlu` çıkar; bu yüzden ders, ekranı kurulum çağrısından sonra denetler. Geri çıkarma sonunda yığıtta yalnız liste kalır. Çıkarılan ayrıntı ekranı bir daha denetlenmez, çünkü artık görünür bir ekran değildir.

#### Özet

- Gezinme denetleyicisi ekranları bir yığıtta tutar; itme yığıta ekler, geri çıkarma üsttekini kaldırır.
- Altta kalan ekran, üstteki ekran modeli değiştirdiğinde kendiliğinden güncellenmez.
- Modelden kalkan bir seçim, listenin etiketinde hâlâ görünüyorsa artıktır.
- İtilen ekran kurulum çağrısı sırasında modele göre uyumlu yazıyor; denetlenmesi bu çağrıdan sonra anlamlıdır.
- Çıkarılan ekran yığıttan ayrılır ve ölçümün dışında kalır.
- Bu dersin uyumu: **artık**.

#### Sonraki Adım

Gezinme denetleyicisi yeni ekranı yığıta ekler. Her ekran yığıta eklenmez: bazıları mevcut ekranın üzerine açılır ve kapanınca yerine bir şey bırakmaz. Sıradaki derste sunum biçimlerini ele alacağız ve bir not penceresinde girilen değerin altta kalan ekrana ulaşıp ulaşmadığını ölçeceğiz.

### Sunum Biçimleri

Source: https://academia.sh/tr/kurslar/uikit/sunum-bicimleri

Bir ekranın üzerine kip sunumla açılan not ekranında girilen değerin modele yazıldığı hâlde alttaki ekrana ulaşmadığının ve ekranda eksik kaldığının ölçümü.

Önceki derste ekranlar bir yığıtta üst üste konuyordu. Bir ekranı göstermenin bir yolu daha vardır: mevcut ekranın üzerine **kip sunum** (modal presentation) olarak açmak. Kip sunum yığıta eklemez; sunulan ekran, sunan ekranın üzerinde durur ve kapatılınca yerini boş bırakır. Yığıta ekleme ise **itmeli sunum** diye ayrılır.

Bu ayrım ölçüm açısından önemlidir, çünkü kip sunumda iki ekran arasında hazır bir bağ yoktur. Sunulan ekranın girdiği değer, sunan ekrana kendiliğinden ulaşmaz. Bu derste girişin modele yazıldığı ama sunan ekranın onu hiç göstermediği bir durumu ölçeriz.

#### Sunmak ve Kapatmak

Aşağıdaki parça bir görev panosunu ve üzerine açılan bir not ekranını gösterir. Platform çerçevesini içe aldığı için çalıştırılmaz.

```swift
import UIKit

final class TaskBoardController: UIViewController {
    let store: TaskStore
    private let titleLabel = UILabel()
    private let openLabel = UILabel()

    init(store: TaskStore) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        titleLabel.text = store.values["title"] ?? ""
        openLabel.text = store.values["open"] ?? ""
    }

    func addNote() {
        let controller = NoteController(store: store)
        controller.modalPresentationStyle = .pageSheet
        present(controller, animated: true)
    }
}

final class NoteController: UIViewController {
    let store: TaskStore
    private let titleLabel = UILabel()
    private let noteLabel = UILabel()

    init(store: TaskStore) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        titleLabel.text = store.values["title"] ?? ""
    }

    func save(_ text: String) {
        store.update("note", text)
        noteLabel.text = text
        dismiss(animated: true)
    }
}
```

`addNote` sunum biçimini seçer ve not ekranını üste açar; `save` hem modele hem kendi etiketine yazar ve kendini kapatır. Panonun denetleyicisinde `note` anahtarını okuyan hiçbir satır yoktur. Sunum biçimi, yani sayfa mı yoksa tam ekran mı olacağı, sunan ekranın kapatmadan sonra yeniden görünme yöntemlerinin çağrılıp çağrılmayacağını da etkileyebilir. Bu ders bunu ölçmez ve ona güvenmez.

#### Sunum Biçimleri

Gezinme yığıtına eklemenin yanında ikinci yol, bir denetleyiciyi başka bir denetleyicinin üstünde **sunmaktır**. Kip sunum, yeni ekranı mevcut ekranın üstüne açar ve kullanıcı kapatana kadar altındaki ekranla etkileşimi keser. Sunum biçimi `modalPresentationStyle` özelliğiyle seçilir: `fullScreen` ekranı baştan sona kaplar, `pageSheet` altındaki ekranı kısmen açıkta bırakan bir sayfa gösterir.

```swift
import UIKit

final class TaskBoardController: UIViewController {
    let store: TaskStore

    init(store: TaskStore) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    func addNote() {
        let note = NoteController(store: store)
        note.modalPresentationStyle = .pageSheet
        present(note, animated: true)
    }
}

final class NoteController: UIViewController {
    let store: TaskStore

    init(store: TaskStore) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    func saveTapped() {
        store.update("note", "Aranacak")
        dismiss(animated: true)
    }
}
```

Sunan denetleyici `present` çağırır, sunulan denetleyici kendini `dismiss` ile kapatır. Sunulan ekran kapandığında sunan ekran yalnızca görünür hale gelir; sunulan denetleyicinin ürettiği sonuç sunana kendiliğinden taşınmaz.

#### Ölçü

Çekirdek önceki derslerle aynıdır.

```swift
// Sync.swift — ekranda gosterilen her alani modelin o anki degeriyle karsilastirir
final class Screen {
    let name: String
    private(set) var shown: [String: String] = [:]

    init(_ name: String) { self.name = name }

    func set(_ key: String, _ value: String) { shown[key] = value }
    func clear(_ key: String) { shown[key] = nil }
}

func verdict(model: String?, shown: String?) -> String {
    switch (model, shown) {
    case (nil, nil): return "olcum-disi"
    case (nil, _?): return "artik"
    case (_?, nil): return "eksik"
    case let (wanted?, displayed?): return wanted == displayed ? "uyumlu" : "bayat"
    }
}

func check(_ label: String, model: [String: String], screen: Screen, keys: [String]) -> [String] {
    keys.map { key in
        let result = verdict(model: model[key], shown: screen.shown[key])
        print("\(label) | \(screen.name).\(key): model=\(model[key] ?? "-") ekran=\(screen.shown[key] ?? "-") -> \(result)")
        return result
    }
}

func countVerdicts(_ verdicts: [String]) {
    for name in ["uyumlu", "bayat", "eksik", "artik"] {
        print("\(name): \(verdicts.filter { $0 == name }.count)")
    }
}
```

#### Not Kaydedilince Pano

Model iki denetleyiciyi saf Swift ile kurar. Model, kodun ekrana ne yazdığını kurar; çerçevenin kendisi değildir. `present` ve `dismiss` yalnız hangi ekranın üstte olduğunu kaydeder. Olaylar şunlardır: pano yüklenir, not ekranı sunulur ve yüklenir, kullanıcı bir not kaydeder ve not ekranı kapanır.

```swift
// main.swift — pano, kip sunum, not
final class TaskStore {
    private(set) var values: [String: String] = [:]
    func update(_ key: String, _ value: String) { values[key] = value }
    func remove(_ key: String) { values[key] = nil }
}

final class TaskBoardController {
    let store: TaskStore
    let screen = Screen("TaskBoardController")
    private(set) var presented: String?

    init(store: TaskStore) { self.store = store }

    func viewDidLoad() {
        screen.set("title", store.values["title"] ?? "")
        screen.set("open", store.values["open"] ?? "")
    }

    func present(_ name: String) { presented = name }
    func dismiss() { presented = nil }
}

final class NoteController {
    let store: TaskStore
    let screen = Screen("NoteController")

    init(store: TaskStore) { self.store = store }

    func viewDidLoad() {
        screen.set("title", store.values["title"] ?? "")
    }

    func save(_ text: String) {
        store.update("note", text)
        screen.set("note", text)
    }
}

let store = TaskStore()
store.update("title", "Rapor")
store.update("open", "4")
let board = TaskBoardController(store: store)
board.viewDidLoad()
let loaded = check("yuklendi", model: store.values, screen: board.screen, keys: ["title", "open"])

let note = NoteController(store: store)
board.present("NoteController")
note.viewDidLoad()
let presented = check("not-sunuldu", model: store.values, screen: note.screen, keys: ["title"])

note.save("Ara")
board.dismiss()
let saved = check("not-kaydedildi", model: store.values, screen: note.screen, keys: ["note"])
let closed = check("not-kapandi", model: store.values, screen: board.screen, keys: ["title", "open", "note"])

countVerdicts(loaded + presented + saved + closed)
```

```
yuklendi | TaskBoardController.title: model=Rapor ekran=Rapor -> uyumlu
yuklendi | TaskBoardController.open: model=4 ekran=4 -> uyumlu
not-sunuldu | NoteController.title: model=Rapor ekran=Rapor -> uyumlu
not-kaydedildi | NoteController.note: model=Ara ekran=Ara -> uyumlu
not-kapandi | TaskBoardController.title: model=Rapor ekran=Rapor -> uyumlu
not-kapandi | TaskBoardController.open: model=4 ekran=4 -> uyumlu
not-kapandi | TaskBoardController.note: model=Ara ekran=- -> eksik
uyumlu: 6
bayat: 0
eksik: 1
artik: 0
```

Not ekranı kendi alanında uyumlu: kullanıcı girdiğini ekranında görüyor ve model aynı değeri taşıyor. Pano ise `title` ve `open` alanlarında uyumlu kaldı, çünkü onları sunum boyunca kimse değiştirmedi. `note` alanı modele yeni girdi ve panonun ekranı onu hiç yazmadı; satır `eksik` çıktı. Sunum kapandığında pano kendiliğinden güncellenmez, ve hangi bilginin kimde olduğunu bilen kod yoktur.

Kip sunum, panoyu çalışır hâlde bırakır ama panoya haber vermez. Haberin ulaşması için sunan ekranın bir araya girmesi gerekir; bunun bilinen yolu bir temsilci protokolüdür. Bu protokol açılış dersinde klasör seçici için kuruldu ve burada aynı biçimde kullanılabilir. Bu derste ölçülen şey eksikliğin kendisidir.

**GD1.** Modelde `save` sunulan ekranın kendi etiketini yazar ve `dismiss` yalnız üstteki ekranı kaldırır. Gerçek çerçevede sunum biçimine göre kapatma sırasında sunan ekranın hangi yaşam döngüsü yöntemlerinin çağrıldığı ölçülmedi; panonun hiçbirinde `note` yazmadığı varsayılır, bu yüzden sonuç çağrılardan bağımsızdır.

#### Sık Yapılan Hatalar

- **Sonucun sunana ulaştığını varsaymak.** Sunulan ekran modeli değiştirir, ama sunan ekran, yeniden göründüğünde yazma çağrısı yoksa eski değeri gösterir. Bu dersin ölçtüğü eksik sonucu budur.
- **Sunulan ekranın sunanı gizleyeceğini beklemek.** `pageSheet` gibi kısmi biçimlerde alttaki ekran yaşam döngüsü açısından kalkmış sayılmaz; `viewWillAppear` dönüşte çağrılmayabilir. Tam ekran biçimde ise çağrılır. Güncellemeyi bu yöntemlere bağlayan kod, biçim değişince sessizce çalışmaz hale gelir.
- **Kapatma düğmesine güvenmek.** Sayfa biçiminde kullanıcı ekranı aşağı çekerek de kapatabilir. Kapatmaya bağlı bir kaydetme çağrısı yalnız düğmede duruyorsa bu yoldan atlanır; kapatma `isModalInPresentation` ile engellenebilir ya da çıkış bir temsilciyle ele alınır.
- **Bir sunum sürerken ikincisini başlatmak.** Çerçeve bunu reddeder ve sunum yapılmaz.
- **Sunulan denetleyiciyi sunan tutarken geri çağrı bağını güçlü kurmak.** Temsilci zayıf tutulmazsa iki denetleyici birbirini serbest bırakmaz.

#### Ölçümü Okumak

Yedi satırın altısı uyumlu, biri eksiktir. Eksik satır, not kaydedilip ekran kapatıldıktan sonra panoda görünmeyen `note` alanıdır: model notu taşıyor, pano ekranı onu hiç yazmadı. Kayıt yapılmıştır; kaybolan, sonucun panoya taşınmasıdır.

Bu sonuçta uyumlu satırların bir anlamı vardır: not ekranının kendi alanları, sunulduğu sürece modelle uyumludur. Hata sunan ve sunulan ekran arasındaki sınırda ortaya çıkmıştır. Kip sunum, iki ekranı ayrı yaşam döngülerine koyar ve aralarındaki taşıma programcıya bırakılır. Çözüm iki ekranı da modelden okutmak, ya da sunulan ekran kapanınca sunanın alanları yeniden yazmasını sağlamaktır; hangisinin seçileceği ekranın yapısına bağlıdır.

#### Sonucu Sunana Taşımak

Sunulan ekranın sonucunu sunana ulaştırmanın olağan yolu bir temsilci protokolüdür. Sunulan denetleyici protokolü çağırır, sunan denetleyici onu uygular. Temsilci referansı zayıf tutulur, çünkü sunan denetleyici sunulanı zaten tutar ve ters yönde güçlü bir bağ bir tutma döngüsü kurardı.

```swift
import UIKit

protocol NoteControllerDelegate: AnyObject {
    func noteController(_ controller: NoteController, didSave text: String)
}

final class NoteController: UIViewController {
    weak var delegate: NoteControllerDelegate?

    func saveTapped() {
        delegate?.noteController(self, didSave: "Aranacak")
        dismiss(animated: true)
    }
}

extension TaskBoardController: NoteControllerDelegate {
    func noteController(_ controller: NoteController, didSave text: String) {
        store.update("note", text)
        // pano alanını burada da yaz
    }
}
```

Temsilci çağrısı iki işi bir araya getirir: sonucu sunana teslim eder ve sunanın ekranı yazması için bir yer açar. Çağrıyı yapan taraf temsilciyi atamadıysa çağrı sessizce atlanır; bu, ilk derste gördüğümüz bağlanmamış temsilci durumunun aynısıdır.

#### Sunum Biçimi ve Kapatma Yolları

Tam ekran biçimde sunan ekran görünümden kalkar ve sunulan kapanınca yeniden görünür; sayfa biçiminde ise alttaki ekran arkada görünmeye devam eder. Bu fark, yaşam döngüsü çağrılarının sunanda ne zaman çalışacağını değiştirir. Alttaki ekranın güncellemesini dönüş çağrısına bağlamak biçim değişince bozulur; güncellemeyi temsilci çağrısına bağlamak ise biçimden bağımsızdır.

Kullanıcı sayfayı aşağı çekerek kapattığında düğmenin eylemi çalışmaz. Kaydetme çağrısı yalnız düğmede duruyorsa bu yol kaydı atlar. `isModalInPresentation = true` yapmak aşağı çekmeyi engeller ve kullanıcıyı açık çıkışlara, yani düğmelere yönlendirir. Bir uyarı kutusu da aynı sunum çağrısıyla gösterilir; uyarı kapanınca sunanın ekranı yine kendi yazma çağrısına bağlıdır.

Sunulan denetleyiciye veri vermek gerektiğinde, veri sunmadan önce, `present` çağrısından önce atanır. Çağrıdan sonra atamak, sunulanın ilk gösteriminde boş veriyle açılmasına yol açabilir.

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

Ölçüm yalnız görünür ekrana bakar. Kapatılan not ekranının nesnesi bellekte durabilir ve içinde eski bir değer taşıyabilir, ama o ekran artık gösterilmediği için denetlenmez. Bu, ekranın ölçüm dışında olduğu anlamına gelir; olcum-disi bir alan değildir.

```swift
// main.swift — kapatilan ekran denetlenmez
final class TaskStore {
    private(set) var values: [String: String] = [:]
    func update(_ key: String, _ value: String) { values[key] = value }
    func remove(_ key: String) { values[key] = nil }
}

final class TaskBoardController {
    let screen = Screen("TaskBoardController")
    private(set) var presented: String?
    func present(_ name: String) { presented = name }
    func dismiss() { presented = nil }
}

final class NoteController {
    let store: TaskStore
    let screen = Screen("NoteController")

    init(store: TaskStore) { self.store = store }

    func save(_ text: String) {
        store.update("note", text)
        screen.set("note", text)
    }
}

let store = TaskStore()
let board = TaskBoardController()
let note = NoteController(store: store)
board.present("NoteController")
note.save("Ara")
board.dismiss()
store.remove("note")
print("sunulan: \(board.presented ?? "yok")")
print("kapanan ekranda kalan: \(note.screen.shown["note"] ?? "-")")
let visible = check("gorunen-pano", model: store.values, screen: board.screen, keys: ["note"])
countVerdicts(visible)
```

```
sunulan: yok
kapanan ekranda kalan: Ara
gorunen-pano | TaskBoardController.note: model=- ekran=- -> olcum-disi
uyumlu: 0
bayat: 0
eksik: 0
artik: 0
```

Not ekranı kapandıktan sonra model notu sildi. Kapanan ekranın nesnesinde `Ara` hâlâ duruyor; çekirdek bu ekranı denetlemiş olsaydı satır artık verirdi. Ama sunulan ekran `yok` ve denetlenen tek ekran pano. Ölçüm bu yüzden sıfır sayı verir ve kapanan ekranın sapmasını raporlamaz. Ekranın ölçümden çıkması, ekranın modelle uyuştuğu anlamına gelmez.

#### Özet

- Kip sunum sunulan ekranı sunan ekranın üzerine koyar ve yığıta eklemez; kapatılınca sunan ekran olduğu gibi kalır.
- Sunulan ekranın girdiği değer modele yazılabilir ama sunan ekrana kendiliğinden ulaşmaz.
- Sunan ekranın hiç yazmadığı bir alan, modele girdiği anda eksik olur.
- Sunum sırasında değişmeyen alanlar sunan ekranda uyumlu kalır.
- Ölçüm yalnız görünür ekrana bakar; kapatılan ekran sayıma girmez.
- Bu dersin uyumu: **eksik**.

#### Sonraki Adım

Şimdiye kadar ekranları kodla kurduk: etiketler denetleyicinin alanlarıydı. Pek çok proje ekranı bir görsel düzenleyicide çizer ve etiketleri dosyadan bağlar. Sıradaki derste görsel düzenleyici ile kodla kurulan arayüzü karşılaştıracağız ve bağlantısı kopan bir etiketin sessizce yazılmadan kaldığı durumu ölçeceğiz.

### Görsel Düzenleyici ve Kodla Arayüz

Source: https://academia.sh/tr/kurslar/uikit/gorsel-duzenleyici-ve-kodla-arayuz

Görsel düzenleyicide çizilen ekranda bağlantısı kopuk bir etiketin yer tutucu metinle bayat kaldığının, kodla kurulan etiketin ise uyumlu olduğunun ve düzenleyicinin kaydettiği yer tutucu metnin artık bıraktığının ölçümü.

Önceki derslerde etiketler denetleyicinin kodla kurduğu alanlardı. Ekran kurmanın ikinci yolu **görsel arayüz düzenleyicisinde** çizmektir: etiketler, düğmeler ve aralarındaki yerleşim bir dosyaya kaydedilir ve uygulama çalışırken dosyadan yüklenir. Üçüncü yol ikisinin karışımıdır: bazı görünümler dosyada, bazıları kodda.

Hangi yolun seçildiği denetleyiciyi değiştirmez: ekrana yazmak yine denetleyicinin işidir. Değişen, denetleyicinin görünüme nasıl ulaştığıdır. Dosyadan gelen görünüm denetleyiciye bir **bağlantı** (outlet) üzerinden verilir; kodla kurulan görünümü denetleyici kendi yaratır. Bu derste ilkinin sessizce kırılabildiği bir durumu ölçeriz.

#### Bağlantı ve Kodla Kurulan Etiket

Aşağıdaki parça bir hava durumu ekranını gösterir. Sıcaklık ve nem etiketleri dosyada çizilmiş ve bağlantıyla verilir; rüzgâr etiketi kodla kurulur. Platform çerçevesini içe aldığı için çalıştırılmaz.

```swift
import UIKit

final class WeatherController: UIViewController {
    let store: WeatherStore
    @IBOutlet var temperatureLabel: UILabel?
    @IBOutlet var humidityLabel: UILabel?
    private let windLabel = UILabel()

    init(store: WeatherStore) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        temperatureLabel?.text = store.values["temperature"] ?? ""
        humidityLabel?.text = store.values["humidity"] ?? ""
        windLabel.text = store.values["wind"] ?? ""
    }
}
```

Bağlantılar isteğe bağlı değer olarak yazıldı. Dosyada bir etiket çizilip denetleyiciye bağlanmadıysa, ya da bağlantı sonradan silindiyse, alan `nil` kalır. Zincirin `?.` biçimi `nil` alana yapılan yazmayı sessizce atlar: ne hata verilir ne uyarı çıkar. Alanı örtük açılan türle tanımlamak aynı durumda hata ile durmayı seçerdi; bu dersin ölçümü zincirli biçimin sonucunu ele alır.

#### Dosyadan Yüklemek

Ekran dosyada çizildiğinde çerçeve dosyayı okur, görünümleri yaratır ve bağlantıları denetleyiciye atar. Atama, `viewDidLoad` çağrılmadan önce biter; bu yüzden `viewDidLoad` içinde bağlantıların dolu olması beklenir. Bağlantıların dolu olmadığı durumda alan `nil` kalır.

Aynı ekran kodla da kurulabilir. Bu durumda görünüm ve etiketler denetleyicinin kendisi tarafından yaratılır ve `nil` olma ihtimalleri yoktur:

```swift
import UIKit

final class WeatherCodeController: UIViewController {
    private let temperatureLabel = UILabel()

    override func loadView() {
        view = UIView()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        temperatureLabel.frame = CGRect(x: 16, y: 80, width: 200, height: 24)
        view.addSubview(temperatureLabel)
    }
}
```

İki yolun karışımı da yaygındır: ana yerleşim dosyada, dinamik parçalar kodda. Hangi yolun seçildiği ölçüm açısından bir fark yaratmaz; fark, yazma çağrısına ulaşan yolun ne kadar kırılgan olduğundadır.

#### Ölçü

Çekirdek önceki derslerle aynıdır.

```swift
// Sync.swift — ekranda gosterilen her alani modelin o anki degeriyle karsilastirir
final class Screen {
    let name: String
    private(set) var shown: [String: String] = [:]

    init(_ name: String) { self.name = name }

    func set(_ key: String, _ value: String) { shown[key] = value }
    func clear(_ key: String) { shown[key] = nil }
}

func verdict(model: String?, shown: String?) -> String {
    switch (model, shown) {
    case (nil, nil): return "olcum-disi"
    case (nil, _?): return "artik"
    case (_?, nil): return "eksik"
    case let (wanted?, displayed?): return wanted == displayed ? "uyumlu" : "bayat"
    }
}

func check(_ label: String, model: [String: String], screen: Screen, keys: [String]) -> [String] {
    keys.map { key in
        let result = verdict(model: model[key], shown: screen.shown[key])
        print("\(label) | \(screen.name).\(key): model=\(model[key] ?? "-") ekran=\(screen.shown[key] ?? "-") -> \(result)")
        return result
    }
}

func countVerdicts(_ verdicts: [String]) {
    for name in ["uyumlu", "bayat", "eksik", "artik"] {
        print("\(name): \(verdicts.filter { $0 == name }.count)")
    }
}
```

#### Bağlantı Kopukken ve Bağlıyken

Model, parçadaki denetleyiciyi saf Swift ile kurar. Model, kodun ekrana ne yazdığını kurar; çerçevenin kendisi değildir. Etiket, adı ve ekranı taşıyan küçük bir tiptir; bağlantılar `main.swift` içinde atanır, çünkü dosyayı yükleyen düzenleyicinin işi budur. Dosyadan gelen iki etiket, düzenleyicide çizildikleri yer tutucu metni (`Etiket`) taşıyarak yüklenir; bunu `loadInterfaceFile` kurar. Sıcaklık etiketi başta bağlanmaz. Sonra bağlantı kurulur ve ekran yeniden yüklenir.

```swift
// main.swift — kopuk baglanti, kodla kurulan etiket
final class WeatherStore {
    private(set) var values: [String: String] = [:]
    func update(_ key: String, _ value: String) { values[key] = value }
}

final class ConnectedLabel {
    let key: String
    let screen: Screen

    init(_ key: String, screen: Screen) {
        self.key = key
        self.screen = screen
    }

    func setText(_ text: String) { screen.set(key, text) }
}

final class WeatherController {
    let store: WeatherStore
    let screen = Screen("WeatherController")
    var temperatureLabel: ConnectedLabel?
    var humidityLabel: ConnectedLabel?
    let windLabel: ConnectedLabel

    init(store: WeatherStore) {
        self.store = store
        windLabel = ConnectedLabel("wind", screen: screen)
    }

    func loadInterfaceFile() {
        screen.set("temperature", "Etiket")
        screen.set("humidity", "Etiket")
    }

    func viewDidLoad() {
        temperatureLabel?.setText(store.values["temperature"] ?? "")
        humidityLabel?.setText(store.values["humidity"] ?? "")
        windLabel.setText(store.values["wind"] ?? "")
    }
}

let store = WeatherStore()
store.update("temperature", "21")
store.update("humidity", "60")
store.update("wind", "12")
let controller = WeatherController(store: store)
controller.humidityLabel = ConnectedLabel("humidity", screen: controller.screen)
controller.loadInterfaceFile()
controller.viewDidLoad()
let broken = check("yuklendi", model: store.values, screen: controller.screen, keys: ["temperature", "humidity", "wind"])

controller.temperatureLabel = ConnectedLabel("temperature", screen: controller.screen)
controller.loadInterfaceFile()
controller.viewDidLoad()
let connected = check("baglandi", model: store.values, screen: controller.screen, keys: ["temperature", "humidity", "wind"])

countVerdicts(broken + connected)
```

```
yuklendi | WeatherController.temperature: model=21 ekran=Etiket -> bayat
yuklendi | WeatherController.humidity: model=60 ekran=60 -> uyumlu
yuklendi | WeatherController.wind: model=12 ekran=12 -> uyumlu
baglandi | WeatherController.temperature: model=21 ekran=21 -> uyumlu
baglandi | WeatherController.humidity: model=60 ekran=60 -> uyumlu
baglandi | WeatherController.wind: model=12 ekran=12 -> uyumlu
uyumlu: 5
bayat: 1
eksik: 0
artik: 0
```

İlk yüklemede sıcaklık `bayat` çıktı: model 21 diyor, ekran dosyadan gelen `Etiket` metnini göstermeye devam ediyor. `temperatureLabel?.setText(...)` çağrısı alan `nil` olduğu için atlandı; etiket ekranda durur, ama kod ona ulaşamaz. Nem bağlı olduğundan ve rüzgâr kodla kurulduğundan uyumlu. Bağlantı kurulup ekran yeniden yüklenince üçü de uyumlu.

Dikkat edilecek nokta, bu hatanın görünürlüğüdür. Kopuk bağlantı derleme hatası vermez, çalışma anında hata vermez, test çıktısında iz bırakmaz; ekranda yalnızca düzenleyicideki yer tutucu metin görünür. Kodla kurulan etiket ise bu yoldan etkilenmez, çünkü onu denetleyici kendi yaratır ve `nil` olamaz. Bu bir yöntemin ötekinden iyi olduğu anlamına gelmez: kodla kurmak bağlantı hatasını ortadan kaldırır, ama yerleşimi düzenleyicide görmenin sağladığı hızı da bırakır.

**GD1.** Modelde bağlantıların atanması `main.swift` içindedir. Gerçek çerçevede bağlantıları dosyanın yüklenmesi sırasında çerçeve atar ve bu atamanın `viewDidLoad` öncesinde bittiği varsayılır; ders bunun sırasını ölçmez.

#### Sık Yapılan Hatalar

- **Bağlantıyı dosyada kopuk bırakmak.** Etiket çizilir, ama denetleyiciye bağlanmaz. Zincirli yazma `nil` alana sessizce hiçbir şey yazmaz ve etikette düzenleyicideki yer tutucu metin kalır.
- **Bağlantı adını kodda değiştirip dosyada güncellememek.** Dosya eski adı hâlâ arar; yükleme sırasında çalışma anında hata alınır. Bu hata kopuk bağlantının tersidir: gürültülüdür ve erken fark edilir.
- **Bağlantıyı kodda silip dosyada bırakmak.** Aynı türden bir uyuşmazlık, aynı sonuç.
- **Yer tutucu metni bırakmak.** Düzenleyicinin etikete yazdığı örnek metin, kod değeri yazana kadar ekranda kalır. Bu dersin sınırlayıcı ölçümü budur.
- **Zayıf ya da güçlü bağlantıyı kural sanmak.** Görünüm zaten kök görünümün ağacında tutulduğu için bağlantı iki biçimde de tanımlanabilir; bu derste ikisi de geçer. Tercih bir hata değildir. Görünümü ağaçtan çıkarıp sonra geri ekleyen kod güçlü bağlantı ister, çünkü zayıf bağlantı ağaçtan çıkan görünümü tutmaz.

Dosyadan gelen alanlar için bir koruma, yazma çağrısını isteğe bağlı zincirle sessizce geçmek yerine, `nil` durumunu ayrıca ele almaktır; bu kursun ölçümü hatanın görünmediğini gösterir, görülmesini sağlamaz.

#### Ölçümü Okumak

Altı satırın beşi uyumlu, biri bayattır; bayat satır bağlantısı kopuk sıcaklık etiketidir. Bağlantı kurulup ekran yeniden yüklenince satır uyumlu olur. Yani alan modelde var, kod onu yazmaya çalışıyor ve ekran hâlâ dosyadaki yer tutucuyu gösteriyor: atlanan yazma, hiç yapılmamış bir yazmadan ayırt edilemiyor.

Bu, kodla kurulan etikete göre bir fark getirir ama bir üstünlük anlamına gelmez. Kodla kurulan etiketin hatası başka yerde çıkar; örneğin görünüme eklenmemişse ekranda görünmez ve eksik sonucu verir. Ölçüm yalnız ekrandaki sonucu sayar.

#### Dosyanın İçinde Neler Kayıtlıdır

Düzenleyicinin kaydettiği dosya yalnızca görünümlerin konumlarını tutmaz. İçinde görünüm ağacı, her görünümün ayarları, kısıtlar, etiketlerin örnek metinleri, denetleyicinin sınıf adı ve bağlantılarla eylemlerin kayıtları bulunur. Bağlantılar ve eylemler özellik ve yöntem adlarıyla dosyada saklanır; kod bu adlardan birini değiştirirse dosya eski adı aramaya devam eder.

Dosyayı belirli bir ad ile yüklemek de mümkündür:

```swift
import UIKit

final class WeatherController: UIViewController {
    @IBOutlet weak var temperatureLabel: UILabel?

    init() {
        super.init(nibName: "WeatherController", bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
}
```

Denetleyicinin sınıf adı dosyada başka bir sınıfa ayarlanmışsa, dosyada kayıtlı bağlantılar yükleme sırasında o sınıfta bulunmayan adlara atanmaya çalışılır ve uygulama çalışma anında bir anahtar hatasıyla çöker. Bu belirti sessiz değildir. Dosyada hiç bağlantı yoksa çökme olmaz; o zaman yalnız kendi sınıfınızın kodu çalışmaz.

#### Hangi Yolu Seçmeli

Seçim, ekibin ve ekranın özelliklerine bağlıdır ve bu derste ölçülmez. Yine de kodun ve dosyanın sınırlarını bilmek gerekir:

- Dosya, yerleşimi çizerek görmeyi sağlar; yerleşim değişiklikleri dosyada metin farkı olarak gelir ve iki kişi aynı ekranı değiştirdiğinde birleştirmesi zordur.
- Kod, her değişikliği satır satır gözden geçirmeyi kolaylaştırır; yerleşimi görmek için çalıştırmak gerekir.
- Ekranın parçaları duruma göre çıkıp giriyorsa, bu parçalar çoğunlukla kodla kurulur.

İki yol bir ekranda birlikte de kullanılabilir. Bu durumda, hangi alanın dosyadan hangisinin kodla geldiğini denetleyicide belirgin tutmak, bir alanın neden `nil` kaldığını aramayı kısaltır.

#### Yaygın Sorunlar ve Belirtileri

Bir ekran dosyayla geldiğinde çıkan sorunların bir kısmı çalışma anında durur, bir kısmı sessizdir: bir alan `nil` kalır ya da yazma çağrısı ekranda olmayan bir nesneye gider. Nedenleri birkaç başlıkta toplanır:

- Bağlantı dosyada vardır ama denetleyicideki özellik silinmiş ya da adı değişmiştir. Dosya yüklenirken adı bulamaz ve çalışma anında bir anahtar hatasıyla durabilir.
- Denetleyicinin sınıfı dosyada ayarlanmamıştır. Sistem düz bir `UIViewController` oluşturur ve kendi sınıfınızın kodu hiç çalışmaz.
- Alan yaşam döngüsünün erken bir noktasında okunur. Bağlantılar görünüm yüklendikten sonra atanır; `init` içinde okunan bir alan `nil` döner.
- Aynı alan hem dosyada hem kodda oluşturulmuştur. İkisinden biri ekrana girer, öteki ise hiç görünmeyen ayrı bir nesne olarak kalır ve yazma çağrıları ona gider.

Bu belirtilerin dördü de modelle ekran arasındaki ayrışmanın kaynaklarıdır. Sessiz olanlarda etikette dosyadaki yer tutucu kalır ve ayrışma ölçümde `bayat` olarak görünür: model bir değer taşır, ekran başka bir değer gösterir. Ölçüm hatanın nedenini söylemez, yalnız ekranın modele ulaşmadığını gösterir.

#### Önizlemeyi Yorumlamak

Düzenleyicideki önizleme, ekranın dosyadaki örnek metinlerle nasıl göründüğünü gösterir. Gerçek veri o sırada yoktur: etiketlerde yazan metin yer tutucudur. Ekran çalışınca model değerleri bu yer tutucuların üstüne yazılır. Yazma çağrısı hiç çalışmazsa, kullanıcı yer tutucu metni görmeye devam eder. Bu durumun ölçümdeki karşılığı alana göre değişir: model o alanı taşıyorsa satır `bayat` olur (ilk ölçümdeki sıcaklık), taşımıyorsa `artık` (aşağıdaki sınırlayıcı ölçümde `caption`).

Bu yüzden yer tutucu metinler gerçek veriye benzemeyecek biçimde seçilir. Etikete gerçek bir ad yazılırsa, yazma çağrısının atlandığı hata önizlemede ve ilk bakışta fark edilmez. Ekran her açıldığında önizlemeye değil, çalışan modele bakmak gerekir; iki kaynağın farkı da ölçümün konusudur. Bu ayrım küçük görünür, ama ekranı yalnız önizlemeye bakarak onaylayan ekiplerde hatanın sürüme kadar taşınmasının başlıca nedenidir.

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

Düzenleyici, bir etiketin metnini dosyaya kaydeder. Bir etiket çizildiğinde içinde yer tutucu bir metin bulunur ve dosyadan yüklenince ekranda bu metin vardır. Kod o etiketi yazmazsa metin ekranda kalır, ve modelde o alanın karşılığı yoktur.

```swift
// main.swift — dosyadan gelen yer tutucu metin
final class WeatherStore {
    private(set) var values: [String: String] = [:]
    func update(_ key: String, _ value: String) { values[key] = value }
}

final class WeatherController {
    let store: WeatherStore
    let screen = Screen("WeatherController")

    init(store: WeatherStore) { self.store = store }

    func loadInterfaceFile() {
        screen.set("caption", "Etiket")
    }

    func viewDidLoad() {
        screen.set("temperature", store.values["temperature"] ?? "")
    }
}

let store = WeatherStore()
store.update("temperature", "21")
let controller = WeatherController(store: store)
controller.loadInterfaceFile()
controller.viewDidLoad()
let placeholder = check("yer-tutucu", model: store.values, screen: controller.screen, keys: ["temperature", "caption"])
countVerdicts(placeholder)
```

```
yer-tutucu | WeatherController.temperature: model=21 ekran=21 -> uyumlu
yer-tutucu | WeatherController.caption: model=- ekran=Etiket -> artik
uyumlu: 1
bayat: 0
eksik: 0
artik: 1
```

`caption` ekranda `Etiket` gösteriyor ve model bu alanı taşımıyor; satır `artik`. Kodu yazan hiçbir şey yanlış yapmadı: etiket dosyada kaldı ve kimse onu silmedi. Kural bunu kaza ile kasıt arasında ayıramaz. Yer tutucu bilerek bırakılmış bir başlık da olabilirdi; kural neden bakmaz.

#### Özet

- Ekran görsel düzenleyicide ya da kodla kurulabilir; her iki yolda da ekrana yazmak denetleyicinin işidir.
- Dosyadan gelen etiket bağlantıyla verilir; bağlantı kopuksa isteğe bağlı zincir yazmayı sessizce atlar ve etiket yer tutucu metinle bayat kalır.
- Kodla kurulan etiket `nil` olamaz ve bu yoldan etkilenmez.
- Bağlantı kurulup ekran yeniden yüklenince alan uyumlu olur.
- Düzenleyicinin dosyaya kaydettiği yer tutucu metin, kod yazmazsa ekranda artık olarak kalır.
- Bu dersin uyumu: **bayat**.

#### Sonraki Adım

Bağlantı denetleyicinin ekrandaki görünüme uzanan elidir. Ters yönde de bir bağ vardır: düğmeye basılması denetleyicideki bir yöntemi çağırır. Sıradaki derste bağlantıları ve eylemleri birlikte ele alacağız ve eylemin modeli güncelleyip etiketi unuttuğu durumu ölçeceğiz.

### Bağlantılar ve Eylemler

Source: https://academia.sh/tr/kurslar/uikit/baglantilar-ve-eylemler

Düğmeye basıldığında çağrılan eylemin yalnızca modeli güncelleyip etiketi unuttuğunda ekranın bayat kaldığının ve biçimlendirilmiş metnin ham değerle karşılaştırıldığında bayat sayıldığının ölçümü.

Önceki derste görünümden denetleyiciye uzanan yolu gördük: denetleyici, bir **bağlantı** (outlet) üzerinden dosyada çizilmiş etikete yazar. Ters yönde de bir bağ vardır. Kullanıcı düğmeye bastığında çerçeve denetleyicide belirli bir yöntemi çağırır; bu çağrıyı kuran bağa **eylem** (action) denir ve **hedef-eylem** düzeninin parçasıdır: düğme hedefi (denetleyici) ve eylemi (yöntem adı) bilir, dokunuşta çağırır.

Bu yöntemin işi genellikle iki parçadır: modeli değiştirmek ve ekrana yansıtmak. İkisinden birini yazmayı unutmak çok kolaydır, çünkü ikisi de aynı yöntemde yan yana durur.

#### Eylem Modeli Güncellediğinde

Aşağıdaki parça bir sepet ekranını gösterir. Adet etiketi bağlantıyla verilir; düğmenin eylemi adedi artırır. Platform çerçevesini içe aldığı için çalıştırılmaz.

```swift
import UIKit

final class CartController: UIViewController {
    let store: CartStore
    @IBOutlet var quantityLabel: UILabel?

    init(store: CartStore) {
        self.store = store
        super.init(nibName: nil, bundle: nil)
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }

    override func viewDidLoad() {
        super.viewDidLoad()
        quantityLabel?.text = store.values["quantity"] ?? ""
    }

    @IBAction func addTapped(_ sender: Any) {
        let next = (Int(store.values["quantity"] ?? "0") ?? 0) + 1
        store.update("quantity", String(next))
    }
}
```

`addTapped` modeli günceller. Etikete bir şey yazmaz. Ekran açılırken etiket doğru okunur; ilk dokunuştan sonra model 2 der, etiket 1 göstermeye devam eder.

#### Eylemi Bağlamak

Eylemi düğmeye bağlamanın iki yolu vardır. Dosyada çizilen düğme için eylem, görsel düzenleyicide düğmeden denetleyicideki yönteme çekilen bir bağla kurulur; yöntem `@IBAction` ile işaretlenir. Kodla kurulan düğme için hedef ve eylem açıkça verilir:

```swift
import UIKit

final class CartCodeController: UIViewController {
    private let addButton = UIButton(type: .system)

    override func viewDidLoad() {
        super.viewDidLoad()
        addButton.addTarget(self, action: #selector(addTapped), for: .touchUpInside)
        view.addSubview(addButton)
    }

    @objc private func addTapped() {
        // modeli güncelle, sonra ekranı yaz
    }
}
```

`#selector` yöntemi adıyla gösterir ve yöntemin `@objc` ile işaretlenmesi gerekir. `.touchUpInside`, parmağın düğmenin içinde kaldırıldığı durumdur; başka olaylar da seçilebilir. Eylem yöntemi isteğe bağlı olarak düğmeyi parametre olarak alır. Eylem ana iş parçacığında çalışır ve ekrana yazmak da orada yapılır.

#### Ölçü

Çekirdek önceki derslerle aynıdır.

```swift
// Sync.swift — ekranda gosterilen her alani modelin o anki degeriyle karsilastirir
final class Screen {
    let name: String
    private(set) var shown: [String: String] = [:]

    init(_ name: String) { self.name = name }

    func set(_ key: String, _ value: String) { shown[key] = value }
    func clear(_ key: String) { shown[key] = nil }
}

func verdict(model: String?, shown: String?) -> String {
    switch (model, shown) {
    case (nil, nil): return "olcum-disi"
    case (nil, _?): return "artik"
    case (_?, nil): return "eksik"
    case let (wanted?, displayed?): return wanted == displayed ? "uyumlu" : "bayat"
    }
}

func check(_ label: String, model: [String: String], screen: Screen, keys: [String]) -> [String] {
    keys.map { key in
        let result = verdict(model: model[key], shown: screen.shown[key])
        print("\(label) | \(screen.name).\(key): model=\(model[key] ?? "-") ekran=\(screen.shown[key] ?? "-") -> \(result)")
        return result
    }
}

func countVerdicts(_ verdicts: [String]) {
    for name in ["uyumlu", "bayat", "eksik", "artik"] {
        print("\(name): \(verdicts.filter { $0 == name }.count)")
    }
}
```

#### Eski ve Düzeltilmiş Eylem

Model, parçadaki iki sürümü saf Swift ile kurar. Model, kodun ekrana ne yazdığını kurar; çerçevenin kendisi değildir. Düğmeye basmak, eylem yönteminin doğrudan çağrılmasıdır. Düzeltilmiş sürümde eylem modeli güncelledikten sonra etiketi de yazar.

```swift
// main.swift — eylem etiketi unutur, eylem etiketi de yazar
final class CartStore {
    private(set) var values: [String: String] = [:]
    func update(_ key: String, _ value: String) { values[key] = value }
}

final class CartController {
    let store: CartStore
    let screen = Screen("CartController")

    init(store: CartStore) { self.store = store }

    func viewDidLoad() {
        screen.set("quantity", store.values["quantity"] ?? "")
    }

    func addTapped() {
        let next = (Int(store.values["quantity"] ?? "0") ?? 0) + 1
        store.update("quantity", String(next))
    }
}

final class FixedCartController {
    let store: CartStore
    let screen = Screen("FixedCartController")

    init(store: CartStore) { self.store = store }

    func viewDidLoad() {
        screen.set("quantity", store.values["quantity"] ?? "")
    }

    func addTapped() {
        let next = (Int(store.values["quantity"] ?? "0") ?? 0) + 1
        store.update("quantity", String(next))
        screen.set("quantity", String(next))
    }
}

let oldStore = CartStore()
oldStore.update("quantity", "1")
let old = CartController(store: oldStore)
old.viewDidLoad()
let oldOpened = check("acildi-eski", model: oldStore.values, screen: old.screen, keys: ["quantity"])
old.addTapped()
let oldTapped = check("dokunuldu-eski", model: oldStore.values, screen: old.screen, keys: ["quantity"])

let fixedStore = CartStore()
fixedStore.update("quantity", "1")
let fixed = FixedCartController(store: fixedStore)
fixed.viewDidLoad()
let fixedOpened = check("acildi-duzeltilmis", model: fixedStore.values, screen: fixed.screen, keys: ["quantity"])
fixed.addTapped()
let fixedTapped = check("dokunuldu-duzeltilmis", model: fixedStore.values, screen: fixed.screen, keys: ["quantity"])

countVerdicts(oldOpened + oldTapped + fixedOpened + fixedTapped)
```

```
acildi-eski | CartController.quantity: model=1 ekran=1 -> uyumlu
dokunuldu-eski | CartController.quantity: model=2 ekran=1 -> bayat
acildi-duzeltilmis | FixedCartController.quantity: model=1 ekran=1 -> uyumlu
dokunuldu-duzeltilmis | FixedCartController.quantity: model=2 ekran=2 -> uyumlu
uyumlu: 3
bayat: 1
eksik: 0
artik: 0
```

Eski sürümde ekran açılırken uyumlu, dokunuştan sonra `bayat`: model 2, ekran 1. Düzeltilmiş sürümde eylem ikisini birlikte yazdığı için dokunuştan sonra da uyumlu. Hata eylemin içinde, tek bir eksik satırdadır ve ekran açılırken görünmez; yalnızca dokunuştan sonra ortaya çıkar.

Bu yüzden eylem yöntemlerini okurken şu soru işe yarar: modeli değiştiren her satırın hemen altında, o değere bakan her ekran alanını da yazan bir satır var mı? Cevap hayırsa ekran, o eylemden sonra modelden geride kalır. Ölçüm aynı soruyu sayıyla sorar: model ile ekranı yan yana okur ve farkı `bayat` olarak işaretler. Eylem birden çok etiketi etkiliyorsa her etiketin kendi satırı olmalıdır; birini yazıp ötekini unutmak aynı hatanın daha sessiz halidir.

**GD1.** Modelde dokunuş, eylem yönteminin doğrudan çağrılmasıdır. Gerçek çerçevede düğmenin hedefi ve eylemi bağlanmadıysa dokunuş hiçbir yöntemi çağırmaz; bu ders o durumu ölçmez.

#### Sık Yapılan Hatalar

- **Yalnız modeli güncellemek.** Eylem değeri değiştirir, etiketi yazmaz. Ekran bu derste ölçüldüğü gibi bayat kalır.
- **Birden çok etiketten birini yazmak.** Eylem toplamı yazar ama adedi unutur; ekranın yarısı uyumlu, yarısı bayat olur.
- **Eylem adını yanlış yazmak.** Dosyada bağlanan yöntemin adı kodda değişirse bağ kopar. Dokunuşun ardından çalışma anında hata alınır ya da hiçbir şey olmaz.
- **`@objc` işaretini atlamak.** Kodla verilen `#selector` bunu ister; işaret yoksa kod derlenmez.
- **Aynı düğmeye iki kez eylem bağlamak.** İki ayrı yöntem sırayla çağrılır ve ikisi de modele yazarsa değer iki kez değişir.
- **Biçimlenmiş değeri modelle kıyaslamak.** Etiket `₺120,00` diyorken model `120` der; ölçümü okuyan bunu hata sanabilir. Karşılaştırmayı aynı biçimden geçirmek gerekir.

#### Ölçümü Okumak

Dört satırın üçü uyumlu, biri bayattır. Bayat satır eski eylemin dokunuştan sonraki halidir: model 2, ekran 1. Aynı ekranın açılış satırı uyumludur ve bu, hatanın nerede olduğunu gösterir: ekranın ilk kurulumunda değil, kullanıcı etkileşiminden sonra.

Bu yüzden bir ekranı yalnız ilk açılışta ölçmek yetmez. Açılış satırı uyumluyken etkileşim satırı bayat olabilir. Düzeltilmiş eylem yazma çağrısını modelin hemen ardına aldığı için iki satır da uyumludur. Kuralın biçimlenmiş metinde bayat demesi ise ayrı bir uyarıdır: sonuç ekranın yanlış olduğunu değil, iki değerin farklı yazıldığını söyler.

#### Gönderen ve Olay Türleri

Eylem yöntemi düğmenin kendisini `sender` parametresiyle alabilir. Parametrenin türü `Any` yerine somut tür olarak yazılırsa, yöntem düğmenin özelliklerine doğrudan ulaşır. Düğme dışındaki denetimler de eylem gönderir; seçim olayı denetimin türüne bağlıdır:

```swift
import UIKit

final class SettingsController: UIViewController {
    @IBOutlet weak var notificationsSwitch: UISwitch?
    @IBOutlet weak var nameField: UITextField?

    @IBAction func switchChanged(_ sender: UISwitch) {
        // sender.isOn modele yazılır, ardından ilgili etiketler yenilenir
    }

    @IBAction func nameEdited(_ sender: UITextField) {
        // sender.text modele yazılır
    }
}
```

Anahtar `valueChanged`, metin alanı `editingChanged` olayıyla eylem çağırır. Bir denetime birden çok eylem bağlanabilir ve `removeTarget` bir bağı koparır. Eylemin kaç kez ve hangi olay için çağrılacağı dosyadaki bağın içindedir; yöntemin kendisine bakmak bunu göstermez.

#### Üç Adım: Oku, Değiştir, Yaz

Bir eylem yöntemi çoğunlukla aynı sırayı izler: denetimden girdiyi okur, modeli değiştirir, ekranda o modele bağlı her alanı yazar. Üçüncü adım atlandığında, bu derste ölçtüğümüz bayatlık ortaya çıkar. Bu adımın atlanma ihtimalini azaltmanın yolu, modele bağlı bütün alanları tek bir yöntemde yazmaktır:

```swift
import UIKit

extension CartController {
    func showModel() {
        quantityLabel?.text = store.values["quantity"] ?? ""
        totalLabel?.text = store.values["total"] ?? ""
    }

    @IBAction func addTapped(_ sender: Any) {
        let next = (Int(store.values["quantity"] ?? "0") ?? 0) + 1
        store.update("quantity", String(next))
        showModel()
    }
}
```

`viewDidLoad` ve her eylem aynı `showModel` yöntemini çağırır. Böylece yeni bir alan eklendiğinde yazma çağrısı tek yere eklenir ve eylemlerden biri onu atlayamaz. Bu düzen hatayı ortadan kaldırmaz; çağrılacak yerin sayısını bire indirir. Ölçüm de her eylemden sonra aynı soruyu sorar: model ile ekran aynı mı.

#### Bağlantı Türleri ve Zayıf Referans

Bağlantılar çoğunlukla `weak` olarak tanımlanır. Görünümü zaten ana görünüm ağacı tutar; denetleyicinin ayrıca güçlü bir referans tutmasına gerek yoktur. Görünüm ağaçtan çıkarıldığında zayıf referans `nil` olur. Bu yüzden bağlantı türü isteğe bağlıdır ve okuma yeri `?` ile yazılır.

Bağlantı olmadan eylem de çalışabilir; ancak eylem içinden etikete ulaşmak için bağlantı gerekir. Eylem yöntemi, yazma işini bağlantı üzerinden yapar. Bağlantı `nil` ise yazma sessizce atlanır ve bu, bu kursun ölçtüğü bayatlığın doğrudan nedenidir.

#### Eylem Adlarında Tutarlılık

Eylem yöntemlerinin adı, ne olduğunu değil ne yaptığını anlatmalıdır. `buttonTapped` yerine `addItemTapped` ya da `saveTapped` yazmak, dosyadaki bağı okuyan kişinin yöntemi açmadan neyin çalışacağını anlamasını sağlar. Dosyada bir düğmeye bağlı eylemin adı değiştirildiyse, bağ eski adla kalır ve düğmeye basınca sistem yöntemi bulamaz.

Bir eylemde birden çok iş yapılıyorsa, işler küçük yöntemlere bölünür ve eylem bu yöntemleri sırayla çağırır. Böylece modeli değiştiren kod ile ekranı yazan kod ayrılır; ölçüm de tam bu iki katmanın birbirine uyup uymadığını sorar.

Bir denetime bağlı eylem yoksa, o denetime basmak hiçbir şey yapmaz ve hata da vermez. Bu, ekranın bayat kalmasının bir diğer yoludur: model değişmemiştir, ekran da öyle. Uyum ölçümü bu durumda `uyumlu` çıkar, ama kullanıcı düğmenin çalışmadığını görür. Bu yüzden ölçüm tek başına yeterli kanıt sayılmaz; her bağlantının ve her eylemin gerçekten bağlı olduğu ayrıca kontrol edilir. Bunu denetleyicide küçük bir kontrol listesiyle yapmak yeterlidir: her denetim için bağlantı var mı, eylem var mı, eylem modeli değiştiriyor mu, ardından ekranı yazıyor mu. Listenin dört sorusu da evet ise ekran modelle uyuşur. Biri hayır ise sorun o adımdadır ve ölçüm sonucu hangi adıma bakılacağını yönlendirir.

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

Kural, model değerini ekran metniyle birebir karşılaştırır. Ekran çoğu zaman modelin kendisini değil biçimlendirilmiş halini gösterir: model `120` der, etiket `₺120,00` yazar. İkisi aynı bilgidir, ama kural bunu bilmez.

```swift
// main.swift — biçimlendirilmiş metin
final class PriceStore {
    private(set) var values: [String: String] = [:]
    func update(_ key: String, _ value: String) { values[key] = value }
}

func formatPrice(_ value: String) -> String { "₺" + value + ",00" }

final class PriceController {
    let store: PriceStore
    let screen = Screen("PriceController")

    init(store: PriceStore) { self.store = store }

    func viewDidLoad() {
        screen.set("price", formatPrice(store.values["price"] ?? ""))
    }
}

let store = PriceStore()
store.update("price", "120")
let controller = PriceController(store: store)
controller.viewDidLoad()
let raw = check("bicimli", model: store.values, screen: controller.screen, keys: ["price"])
let formattedModel = ["price": formatPrice(store.values["price"] ?? "")]
let formatted = check("bicimli-karsilastirilmis", model: formattedModel, screen: controller.screen, keys: ["price"])
countVerdicts(raw + formatted)
```

```
bicimli | PriceController.price: model=120 ekran=₺120,00 -> bayat
bicimli-karsilastirilmis | PriceController.price: model=₺120,00 ekran=₺120,00 -> uyumlu
uyumlu: 1
bayat: 1
eksik: 0
artik: 0
```

Ham değerle karşılaştırma `bayat` verdi, ekran ise doğruydu. Modeli de aynı biçimlendirmeden geçirince satır uyumlu çıktı. Kuralın sınırı budur: biçimlendirmeyi bilmez. Bu yüzden `bayat` bulgusu bir ekranın yanlış olduğunu değil, iki değerin farklı yazıldığını söyler; hangisinin doğru olduğuna bakmak ölçümü okuyanın işidir.

#### Özet

- Bağlantı denetleyiciden görünüme, eylem görünümden denetleyiciye uzanır; eylem hedef-eylem düzeniyle çağrılır.
- Modeli güncelleyip etiketi yazmayan eylem ekranı bayat bırakır.
- Hem modeli hem etiketi yazan eylem dokunuştan sonra da uyumludur.
- Ekran açılırken uyumlu görünen bir denetleyici, dokunuştan sonra bayat olabilir.
- Biçimlendirilmiş metin ham değerle karşılaştırılınca bayat çıkar; iki taraf aynı biçimden geçirilince uyumlu olur.
- Bu dersin uyumu: **bayat**.

#### Sonraki Adım

Bu ders görünüm denetleyicisini, yaşam döngüsünü, gezinmeyi, sunumu ve bağlantıları kapattı. Sıradaki derslerde ekranın düzenine geçeceğiz: kısıt tabanlı düzende bir görünümün boyutunu ve konumunu kurallarla anlatmayı ele alacağız.

## Düzen ve Koleksiyonlar

Kısıtlar, öncelikler ve belirsizlik çözümüyle kısıt tabanlı düzen, kısıt sayısını azaltan yığıt görünümleri, hücre yeniden kullanımı ve veri kaynağıyla tablo görünümleri, esnek düzen tanımlı koleksiyon görünümleri, sorumluluk devreden temsilci kalıbı, çizim ve katman animasyonuyla alt düzey görsel denetim.

### Kısıt Tabanlı Düzen

Source: https://academia.sh/tr/kurslar/uikit/kisit-tabanli-duzen

Düzeni besleyen kısıt sabitinin modelde değiştiği ama ekrandaki kısıta yazılmadığı durumun bayat, yeni kısıtın hiç kurulmadığı durumun eksik sayıldığının ölçümü.

Önceki konunun son dersi, Bağlantılar ve Eylemler, ekrandaki bir görünümü koda nasıl bağladığımızı anlattı. Bu konuda bağlanan şey görünümün kendisi değil, **yerleşimidir**. UIKit'te yerleşimi çoğu zaman **kısıt** (constraint) verir: bir görünümün kenarının ya da genişliğinin başka bir şeye göre nasıl durduğunu söyleyen kural. Kısıt, bir sabit taşır ve bu sabit kodda bir sayıdır. Model o sayıyı değiştirdiğinde ekrandaki kısıt kendiliğinden yeni sayıyı bilmez.

Bu dersin sorusu hesabın doğruluğu değil, **uyumdur**: model bir boşluğu ya da genişliği değiştirdi, ekran o değeri aldı mı. Ölçüm çekirdek dosyayla yapılır.

```swift
// Sync.swift — ekranda gosterilen her alani modelin o anki degeriyle karsilastirir
final class Screen {
    let name: String
    private(set) var shown: [String: String] = [:]

    init(_ name: String) { self.name = name }

    func set(_ key: String, _ value: String) { shown[key] = value }
    func clear(_ key: String) { shown[key] = nil }
}

func verdict(model: String?, shown: String?) -> String {
    switch (model, shown) {
    case (nil, nil): return "olcum-disi"
    case (nil, _?): return "artik"
    case (_?, nil): return "eksik"
    case let (wanted?, displayed?): return wanted == displayed ? "uyumlu" : "bayat"
    }
}

func check(_ label: String, model: [String: String], screen: Screen, keys: [String]) -> [String] {
    keys.map { key in
        let result = verdict(model: model[key], shown: screen.shown[key])
        print("\(label) | \(screen.name).\(key): model=\(model[key] ?? "-") ekran=\(screen.shown[key] ?? "-") -> \(result)")
        return result
    }
}

func countVerdicts(_ verdicts: [String]) {
    for name in ["uyumlu", "bayat", "eksik", "artik"] {
        print("\(name): \(verdicts.filter { $0 == name }.count)")
    }
}
```

Çekirdekte `Screen`, ekrana en son yazılan değerleri anahtar başına tutar. `verdict` modelin değerini ekranın değeriyle karşılaştırır ve dört sonuçtan birini verir: ikisi de aynıysa `uyumlu`, farklıysa `bayat`, model değer taşıyıp ekran taşımıyorsa `eksik`, ekran taşıyıp model taşımıyorsa `artik`. Çekirdek bir kısıt çözücü değildir; model, kodun ekrana ne yazdığını kurar, çerçevenin kendisi değildir.

#### Kısıtın Taşıdığı Sabit

Bir fatura kartının kenar boşluğunu ve genişliğini düşünelim. İkisi de kodda birer kısıtın sabitidir. Kart oluşurken kısıtlar bir kez kurulur ve sabitler o anki değerleri alır.

```swift
import UIKit

final class InvoiceCardView: UIView {
    let paddingConstraint = NSLayoutConstraint()
    let widthConstraint = NSLayoutConstraint()

    func apply(padding: CGFloat, width: CGFloat) {
        paddingConstraint.constant = padding
        widthConstraint.constant = width
    }
}
```

Buradaki `apply(padding:width:)` yöntemi iki sabiti yazar. Kritik nokta, sabitlerin yalnız bu yöntem çağrıldığında değişmesidir. Model sonradan boşluğu büyütse ve yöntem çağrılmasa, kısıt eski sayıyı taşımaya devam eder.

Ölçümde model iki alan taşır: `sidePadding` ve `cardWidth`. Ekran, kısıtlara yazılan sabitleri tutan bir `Screen`'dir. Üç adım verilir: ilk kurulum, modelin boşluğu değiştirmesi, ve sabitin güncellenmesi.

```swift
// main.swift — model boslugu degistirir, kisit guncellenmedikce bayat kalir
let invoice = Screen("InvoiceCard")
var model = ["sidePadding": "16", "cardWidth": "320"]

invoice.set("sidePadding", "16")
invoice.set("cardWidth", "320")
let first = check("ilk-kurulum", model: model, screen: invoice, keys: ["sidePadding", "cardWidth"])

model["sidePadding"] = "24"
let second = check("bosluk-degisti", model: model, screen: invoice, keys: ["sidePadding", "cardWidth"])

invoice.set("sidePadding", "24")
let third = check("kisit-guncellendi", model: model, screen: invoice, keys: ["sidePadding", "cardWidth"])

countVerdicts(first + second + third)
```

```
ilk-kurulum | InvoiceCard.sidePadding: model=16 ekran=16 -> uyumlu
ilk-kurulum | InvoiceCard.cardWidth: model=320 ekran=320 -> uyumlu
bosluk-degisti | InvoiceCard.sidePadding: model=24 ekran=16 -> bayat
bosluk-degisti | InvoiceCard.cardWidth: model=320 ekran=320 -> uyumlu
kisit-guncellendi | InvoiceCard.sidePadding: model=24 ekran=24 -> uyumlu
kisit-guncellendi | InvoiceCard.cardWidth: model=320 ekran=320 -> uyumlu
uyumlu: 5
bayat: 1
eksik: 0
artik: 0
```

`ilk-kurulum` satırlarında iki alan da uyumlu: model ve ekran aynı sayıyı taşıyor. `bosluk-degisti` satırında `sidePadding` için model 24, ekran 16 ve sonuç `bayat`. Ekran hâlâ eski sabiti tutuyor; `cardWidth` ise hiç değişmediği için uyumlu kalıyor. `kisit-guncellendi` adımında sabit yazılınca iki alan yeniden uyumlu.

Sayım altı denetimden çıkar: beş uyumlu, bir bayat. Bayat olan tek satır, modelin değiştiği ama kısıta yazılmadığı adımdır. Hiçbir satırda eksik ya da artık yok, çünkü iki tarafta da iki anahtar var.

**DZ1.** Bu ölçüm sabitin metin olarak eşitliğini karşılaştırır. Kısıtın etkinleştirilip etkinleştirilmediği, başka bir kısıtla çakışıp çakışmadığı ölçülmedi.

#### Hiç Kurulmayan Kısıt

Bayat, ekranın bir değeri taşıdığı ama eskidiği durumdur. Daha sessiz bir durum, modelin bir değer taşıması ve ekranda bu değeri karşılayan kısıtın hiç kurulmamasıdır. Kartın altına bir dipnot alanı eklendiğini düşünelim: model yüksekliğini söylüyor, ama kod henüz o yükseklik kısıtını oluşturmadı.

```swift
// main.swift — model yukseklik verir, kisit kurulmadikca eksik sayilir
let card = Screen("InvoiceCard")
let model = ["sidePadding": "24", "footerHeight": "44"]

card.set("sidePadding", "24")
let before = check("dipnot-eklendi", model: model, screen: card, keys: ["sidePadding", "footerHeight"])

card.set("footerHeight", "44")
let after = check("kisit-kuruldu", model: model, screen: card, keys: ["sidePadding", "footerHeight"])

countVerdicts(before + after)
```

```
dipnot-eklendi | InvoiceCard.sidePadding: model=24 ekran=24 -> uyumlu
dipnot-eklendi | InvoiceCard.footerHeight: model=44 ekran=- -> eksik
kisit-kuruldu | InvoiceCard.sidePadding: model=24 ekran=24 -> uyumlu
kisit-kuruldu | InvoiceCard.footerHeight: model=44 ekran=44 -> uyumlu
uyumlu: 3
bayat: 0
eksik: 1
artik: 0
```

`dipnot-eklendi` adımında `footerHeight` için model 44, ekran `-` ve sonuç `eksik`. Ekranın bu anahtar için hiçbir değeri yok. `kisit-kuruldu` adımında kısıt yazılınca satır uyumlu oluyor. Toplam üç uyumlu ve bir eksik; bayat yok, çünkü ekranda yanlış bir sayı hiç görünmedi, yalnızca sayı hiç yoktu.

Eksik ile bayat kodda birbirine benzer görünür: ikisinde de kullanıcı yanlış bir yerleşim görür. Ayrım nedeninde yatar. Bayat, yazma adımının atlandığını, eksik ise kurma adımının hiç yazılmadığını söyler.

**DZ2.** Bu ölçüm, dipnot alanının yalnız tek bir anahtarla temsil edildiği durum için. Bir kısıt grubunun bir kısmının kurulup bir kısmının kurulmadığı ölçülmedi.

#### Kısıtlar Gerçekte Nasıl Çözülür

Ölçümde bir kısıt tek bir sayıya indirgendi, ama çerçevenin kendisi kısıtları bir denklem sistemi olarak çözer. Her görünümün konumu ve boyutu dört değerle belirlenir: sol kenar, üst kenar, genişlik ve yükseklik. Kısıtlar bu değerler arasındaki ilişkileri yazar. Bir görünüm için bu dört değeri tek anlamlı biçimde belirleyen kısıt kümesi eksiksizdir. Eksik kalırsa **belirsizlik** (ambiguity) oluşur: çerçeve görünümü iki ya da daha fazla geçerli yere koyabilir ve hangisini seçeceği kuralla belirlenmemiştir. Fazla kalırsa **çelişki** (conflict) oluşur: iki kısıt aynı değeri farklı istemektedir ve biri çalışma anında kırılır.

Kırılacak kısıtı **öncelik** (priority) belirler. Öncelik 1 ile 1000 arasında bir sayıdır; 1000 zorunlu kısıttır ve çerçeve onu kırmadan çözüm arar. Daha düşük öncelikli bir kısıt, yüksek öncelikli kısıtlarla çatıştığında gevşer. Çerçeve kırdığı kısıtı çıktıya yazar, ama ekran yine de çizilir; bu yüzden çatışma çoğu zaman yalnız bir günlük satırı olarak kalır ve fark edilmez.

```swift
import UIKit

func pinCard(_ card: UIView, in container: UIView) {
    card.translatesAutoresizingMaskIntoConstraints = false
    let width = card.widthAnchor.constraint(equalToConstant: 320)
    width.priority = .defaultHigh
    NSLayoutConstraint.activate([
        card.topAnchor.constraint(equalTo: container.topAnchor, constant: 16),
        card.centerXAnchor.constraint(equalTo: container.centerXAnchor),
        card.leadingAnchor.constraint(greaterThanOrEqualTo: container.leadingAnchor, constant: 16),
        width
    ])
}
```

Bu parçada genişlik için öncelik 750 verilmiştir. Kapsayıcı dardır ve sol kenar kısıtı zorunlu kaldığı sürece genişlik kısıtı gevşer; kapsayıcı genişse 320 olarak uygulanır. Böylece tek bir kısıt kümesi iki duruma da uyar. Dikkat edilecek nokta, `translatesAutoresizingMaskIntoConstraints` değerinin kapatılmasıdır: kodla kurulan görünümde bu değer açık bırakılırsa çerçeve görünümün eski biçimdeki otomatik kısıtlarını da ekler ve bunlar yazdığınız kısıtlarla çelişir.

#### İçsel Boyut ve Sıkıştırma Dirençleri

Her görünüm kısıtsız bir boyut da taşıyabilir. Bir etiketin metni kadar, bir düğmenin başlığı kadar yer istemesi buna örnektir; bu isteğe **içsel boyut** (intrinsic content size) denir. İçsel boyutu olan bir görünüm için genişlik ya da yükseklik kısıtı yazmak çoğu zaman gerekmez. Alan yetmediğinde ya da fazla geldiğinde görünümün ne yapacağını iki değer söyler: **büyüme direnci** (content hugging) görünümün içsel boyutundan büyümeye ne kadar karşı koyduğunu, **sıkışma direnci** (compression resistance) küçülmeye ne kadar karşı koyduğunu bildirir. İki etiket yan yana ve alan dar olduğunda, sıkışma direnci düşük olan etiket kısalır; ikisi eşitse çerçeve hangisini seçeceğini bilemez ve belirsizlik doğar.

Bu yüzden yan yana duran iki değişken uzunluklu metin için bir etikete açıkça daha yüksek direnç vermek gerekir. Ölçümdeki `FlexibleCard` bu esnekliğin modele yansımamasını gösteriyordu: direnç değerleri modelde yoktur, ekranda ise belirleyicidir.

#### Kısıtı Kodla mı Düzenleyiciyle mi Kurmalı

Kısıt, görsel düzenleyicide de kodda da kurulabilir ve ikisi aynı sonucu verir. Düzenleyici, değişmeyen yapılar için kısadır: kenar boşlukları, sabit yükseklikler ve hizalamalar bir kez çizilir ve dosyada saklanır. Kod ise çalışma anında değişen düzenlerde işe yarar: bir bölümün koşullu görünmesi, içeriğe göre değişen yükseklik ya da kullanıcı ayarına göre kayan boşluk bunlardandır. Kodla kurulan kısıt bir değişkende saklanırsa sonradan `constant` değeri değiştirilebilir ve çerçeve düzeni sonraki turda yeniden çözer. Bu dersin ilk iki ölçümü tam olarak bu değişkenin güncellenmesi ile ilgilidir. Kararı basit tutmak için şu ölçü yeterlidir: değer kurulduktan sonra değişmeyecekse düzenleyici, model ile birlikte değişecekse kod seçilir.

Karma kullanımda tek kural, bir görünümün kısıtlarının tek kaynaktan gelmesidir. Düzenleyicide kurulmuş bir kısıtı kodda değiştirmek gerekirse, kısıt bir bağlantıyla (outlet) koda bağlanır ve yeni bir kısıt eklemek yerine mevcut kısıtın sabiti güncellenir.

#### Sık Yapılan Hatalar

- Kısıtı kurup etkinleştirmeyi unutmak. Oluşturulan bir kısıt, etkinleştirilmediği sürece hiçbir şey yapmaz. Etkinleştirme `isActive` ile ya da `NSLayoutConstraint.activate` ile yapılır.
- Aynı görünüm için iki ayrı yerde kısıt kurmak. Biri kod, biri görsel düzenleyici kaynaklıysa çelişki doğar ve hangisinin kırıldığı çalıştırmaya göre değişebilir.
- Model değişince kısıtın sabitini güncellemeyi unutmak. Bu dersin ilk ölçümü tam bu durumdur: sabit kurulduğu andaki değeri taşır, model sonradan değişse de kendiliğinden izlemez.
- Gizlenen bir görünümün kısıtlarını bırakmak. Görünüm gizlense de kısıtları yerinde kalır ve boşluk korunur; boşluğun kapanmasını istiyorsanız kısıtı ya da yığıt görünümünü kullanmak gerekir.
- Öncelik sayısını tahmini bir değere bağlamak. Bire bir aynı öncelikli iki kısıt çatışırsa sonuç tanımlı değildir; öncelikleri birbirinden net ayırmak gerekir.

#### Ölçülen Sonucun Yorumu

İlk ölçümün bayat sonucu çerçevenin bir hatası değildir; kısıtın tanımı gereği böyledir. Kısıt bir ilişki değil bir değer olarak saklandığında, ilişkiyi güncelleyecek kodu yazmak geliştiricinin işidir. İkinci ölçümün eksik sonucu da aynı kökten gelir, ama ters yönden: model bir değer verir, ekranda onu taşıyacak kısıt hiç kurulmamıştır. Üçüncü ölçüm ise önceliğin modelde olmadığını gösterir; iki ekranda aynı sabit aynı uyumu verir ve önceliğin farkı çıktıya girmez. Üç sonuçtan çıkarılacak ders şudur: modelle ekranı karşılaştırırken kısıtın değeri görülebilir, ama onun çerçevedeki ağırlığı görülemez.

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

Kısıtın iki özelliği, **öncelik** ve **belirsizlik**, çözücünün çatışmaları nasıl çözeceğini belirler. Öncelik düşük bir kısıt, gerekirse esner. UIKit'te bu iki özellik koddan okunur.

```swift
import UIKit

func makeWidthConstraint(for card: UIView, priority: UILayoutPriority) -> NSLayoutConstraint {
    let constraint = card.widthAnchor.constraint(equalToConstant: 320)
    constraint.priority = priority
    return constraint
}
```

Modelin böyle bir alanı yok. Model yalnız sabiti karşılaştırır: kısıt zorunlu da olsa, düşük öncelikli de olsa sabit aynıysa sonuç aynıdır. Bunu aynı sabite sahip iki ekranla gösterelim; biri zorunlu, öteki düşük öncelikli bir kısıtla kurulmuş gibi düşünülür. Önceliği taşıyacak bir anahtar yok, bu yüzden ikisi de yalnız `cardWidth` yazar.

```swift
// main.swift — oncelik modelde yok, ayni sabit iki ekranda da ayni uyumu verir
let model = ["cardWidth": "320"]
let required = Screen("RequiredCard")
let flexible = Screen("FlexibleCard")

required.set("cardWidth", "320")
flexible.set("cardWidth", "320")

let requiredResult = check("zorunlu", model: model, screen: required, keys: ["cardWidth"])
let flexibleResult = check("esnek", model: model, screen: flexible, keys: ["cardWidth"])

countVerdicts(requiredResult + flexibleResult)
```

```
zorunlu | RequiredCard.cardWidth: model=320 ekran=320 -> uyumlu
esnek | FlexibleCard.cardWidth: model=320 ekran=320 -> uyumlu
uyumlu: 2
bayat: 0
eksik: 0
artik: 0
```

İki satır da uyumlu ve sayım iki uyumlu. Önceliğin farkı çıktıda görünmedi. Bu bir sınırdır: **kısıt çözücü ölçülmez**. Model kısıtın taşıdığı sabiti karşılaştırır, ekranın çözücüden sonra aldığı boyutu değil. Bir kısıt düşük öncelikle ezilmiş olabilir ve ekrandaki boyut yine de modelden farklı çıkabilir; bu fark bu dersin ölçüsünün dışındadır.

**DZ3.** Bu ölçüm yalnız sabitin eşitliği için. Öncelik ve belirsizlik KAYNAK parçada gösterildi, çıktıya girmedi.

#### Özet

- Kısıt tabanlı düzende ekranın modelden aldığı şey kısıtın sabitidir; model değişince sabit kodla yeniden yazılmalıdır.
- Modelde değişen ama kısıta yazılmayan değer bayattır: ekran eski sayıyı taşır.
- Modelde olup hiç kurulmamış bir kısıt eksiktir: ekranın o anahtar için değeri yoktur.
- Öncelik ve belirsizlik modelde yoktur; aynı sabit zorunlu ya da esnek kısıtta aynı uyumu verir.
- Model kısıt çözücüyü ölçmez; sabiti karşılaştırır, çözülmüş boyutu değil.
- Bu dersin uyumu: **bayat**.

#### Sonraki Adım

Kısıtları tek tek kurmak, görünüm çoğaldıkça ağırlaşır. Sıradaki derste bir yığıt görünümü kısıtları kendi üstlenir; yazılan şey bir alt görünümün gizlenip gizlenmediği olur. O derste uyum yeni bir adla anılır: artık.

### Yığıt Görünümleri

Source: https://academia.sh/tr/kurslar/uikit/yigit-gorunumleri

Modelden kalkan bir rozetin yığıt görünümünde gizlenmediği durumun artık, gizleme yazıldığında uyumlu sayıldığının ve gizlemenin modelde görünürlük alanı olarak kurulduğunun ölçümü.

Önceki derste, Kısıt Tabanlı Düzen, bir kısıtın sabitini elle yazmanın ekranın modelden geri kalmasına yol açtığını ölçtük. Kısıtları tek tek kurmak görünüm çoğaldıkça ağırlaşır. **Yığıt görünümü** (stack view) bu yükü azaltır: alt görünümleri bir eksen boyunca dizer ve aralarındaki boşluğu kendisi hesaplar. Bu dersin ölçüsü başka bir yere düşer. Yığıt görünümünde ekrandan bir şey kaldırmak, onu gizlemek demektir ve gizlemeyi kod yapar.

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

```swift
// Sync.swift — ekranda gosterilen her alani modelin o anki degeriyle karsilastirir
final class Screen {
    let name: String
    private(set) var shown: [String: String] = [:]

    init(_ name: String) { self.name = name }

    func set(_ key: String, _ value: String) { shown[key] = value }
    func clear(_ key: String) { shown[key] = nil }
}

func verdict(model: String?, shown: String?) -> String {
    switch (model, shown) {
    case (nil, nil): return "olcum-disi"
    case (nil, _?): return "artik"
    case (_?, nil): return "eksik"
    case let (wanted?, displayed?): return wanted == displayed ? "uyumlu" : "bayat"
    }
}

func check(_ label: String, model: [String: String], screen: Screen, keys: [String]) -> [String] {
    keys.map { key in
        let result = verdict(model: model[key], shown: screen.shown[key])
        print("\(label) | \(screen.name).\(key): model=\(model[key] ?? "-") ekran=\(screen.shown[key] ?? "-") -> \(result)")
        return result
    }
}

func countVerdicts(_ verdicts: [String]) {
    for name in ["uyumlu", "bayat", "eksik", "artik"] {
        print("\(name): \(verdicts.filter { $0 == name }.count)")
    }
}
```

Çekirdek, ekranda gösterilen her alanı modelin o anki değeriyle karşılaştırır. Model, kodun ekrana ne yazdığını kurar; çerçevenin kendisi değildir.

#### Düzenden Düşen Alt Görünüm

Bir sipariş satırı düşünelim: üstte sipariş başlığı, yanında yeni siparişleri işaretleyen bir rozet. İkisi bir yığıt görünümünde yan yana durur. Rozet modelden kalktığında, yani sipariş artık yeni sayılmadığında, rozetin görünümü yığıtta durmaya devam eder.

```swift
import UIKit

final class OrderRowView: UIView {
    let titleLabel = UILabel()
    let badgeLabel = UILabel()
    let rowStack = UIStackView()

    func configure(title: String, badge: String?) {
        titleLabel.text = title
        badgeLabel.text = badge
    }
}
```

`configure(title:badge:)` rozet yoksa metni boşaltmaz, yalnız yeni değeri atar; ayrıca `badgeLabel.isHidden` ayarlanmaz. Bu yüzden rozet modelden kalktığında ekranda eski metin kalır. Ölçümde üç adım var: sipariş yüklenir, rozet modelden kalkar, sonra ekran rozeti gizler.

```swift
// main.swift — modelden kalkan rozet gizlenmezse artik, gizlenince olcum-disi kalir
let row = Screen("OrderRow")
var model = ["title": "Siparis 1042", "badge": "Yeni"]

row.set("title", "Siparis 1042")
row.set("badge", "Yeni")
let loaded = check("siparis-yuklendi", model: model, screen: row, keys: ["title", "badge"])

model["badge"] = nil
let removed = check("rozet-kalkti", model: model, screen: row, keys: ["title", "badge"])

row.clear("badge")
let hidden = check("rozet-gizlendi", model: model, screen: row, keys: ["title", "badge"])

countVerdicts(loaded + removed + hidden)
```

```
siparis-yuklendi | OrderRow.title: model=Siparis 1042 ekran=Siparis 1042 -> uyumlu
siparis-yuklendi | OrderRow.badge: model=Yeni ekran=Yeni -> uyumlu
rozet-kalkti | OrderRow.title: model=Siparis 1042 ekran=Siparis 1042 -> uyumlu
rozet-kalkti | OrderRow.badge: model=- ekran=Yeni -> artik
rozet-gizlendi | OrderRow.title: model=Siparis 1042 ekran=Siparis 1042 -> uyumlu
rozet-gizlendi | OrderRow.badge: model=- ekran=- -> olcum-disi
uyumlu: 4
bayat: 0
eksik: 0
artik: 1
```

`siparis-yuklendi` adımında iki alan uyumlu. `rozet-kalkti` adımında `title` hâlâ uyumlu; `badge` için model `-`, ekran `Yeni` ve sonuç `artik`. Ekranda, modelin artık karşılığı olmayan bir değer kalmış. `rozet-gizlendi` adımında ekran da `-` olunca satır `olcum-disi` basar: iki tarafta da olmayan alan ne uyumlu ne artık sayılır.

Altı satırın sayımı beşi kapsar: dört uyumlu, bir artık. Altıncı satır `olcum-disi` olduğu için hiçbir sayıya girmez; sayımın dört adından hiçbiri onu içermez.

**DZ1.** Bu ölçüm rozetin ekranda var olup olmadığını anahtarın değeriyle temsil eder. Yığıt görünümünün alt görünümü gerçekten ağaçta tutup tutmadığı ölçülmedi.

#### Gizlemek Bir Alandır

Gizleme, alt görünümün `isHidden` özelliğine yazılan bir değerdir. Yığıt görünümü gizli alt görünümü düzenden çıkarır ve kalan görünümler boşluğu kapatır. Yani ekranın "rozet görünüyor mu" bilgisi ayrı bir değer olarak durur ve model bunu bir görünürlük alanı olarak tutabilir.

```swift
import UIKit

extension OrderRowView {
    func showBadge(_ isVisible: Bool) {
        badgeLabel.isHidden = !isVisible
    }
}
```

Modelde `badgeVisible` anahtarı `evet` ya da `hayir` değerini taşır. Model rozeti gizlemeyi istediğinde ve ekran bu çağrıyı almadığında iki taraf ayrışır.

```swift
// main.swift — gorunurluk alani: model gizle der, ekran gizlemezse bayat kalir
let row = Screen("OrderRow")
var model = ["badgeVisible": "evet"]

row.set("badgeVisible", "evet")
let shown = check("rozet-gorunur", model: model, screen: row, keys: ["badgeVisible"])

model["badgeVisible"] = "hayir"
let stale = check("model-gizledi", model: model, screen: row, keys: ["badgeVisible"])

row.set("badgeVisible", "hayir")
let synced = check("ekran-gizledi", model: model, screen: row, keys: ["badgeVisible"])

countVerdicts(shown + stale + synced)
```

```
rozet-gorunur | OrderRow.badgeVisible: model=evet ekran=evet -> uyumlu
model-gizledi | OrderRow.badgeVisible: model=hayir ekran=evet -> bayat
ekran-gizledi | OrderRow.badgeVisible: model=hayir ekran=hayir -> uyumlu
uyumlu: 2
bayat: 1
eksik: 0
artik: 0
```

İkinci satırda model `hayir`, ekran `evet` ve sonuç `bayat`. Görünürlük bir değer olarak durduğunda, onu atlamak artık değil bayat verir: iki tarafta da bir değer var, yalnız eski. Aynı konu iki biçimde ölçüldü ve iki ayrı kategori çıktı. Alan modelden tümüyle kalktığında artık, değeri değiştiğinde bayat.

**DZ2.** Bu ölçüm `evet` ve `hayir` metinleriyle görünürlüğü temsil eder. `isHidden` değerinin bir `Bool` olduğu ve metin karşılaştırmasının kodun ekrana yazdığını yansıttığı varsayıldı.

#### Yığıt Görünümünün Dağıtımı

Yığıt görünümü, alt görünümlerini bir eksen boyunca sıralayan ve aralarındaki boşlukları kendisi hesaplayan bir kapsayıcıdır. Eksen `axis` ile seçilir: yatay ya da dikey. Alt görünümlerin eksen boyunca nasıl yayılacağını **dağıtım** (distribution) belirler, eksene dik yöndeki yerleşimi ise **hizalama** (alignment) belirler. Bu ayrım önemlidir, çünkü iki ayar birbirinden bağımsızdır ve en sık karışan nokta da budur.

Dağıtımın beş değeri vardır. `fill` alt görünümlerden birini uzatarak alanı doldurur; hangisinin uzayacağını büyüme dirençleri söyler. `fillEqually` hepsine eşit boyut verir. `fillProportionally` içsel boyutlarla orantılı dağıtır. `equalSpacing` boyutlara dokunmadan aradaki boşlukları eşitler. `equalCentering` ise görünümlerin merkezleri arasındaki uzaklığı eşitler. Hizalama ise `fill`, `leading`, `center`, `trailing` gibi değerlerle dik yöndeki konumu seçer.

```swift
import UIKit

func makeRow(title: String, badge: UIView) -> UIStackView {
    let label = UILabel()
    label.text = title
    let row = UIStackView(arrangedSubviews: [label, badge])
    row.axis = .horizontal
    row.alignment = .center
    row.distribution = .fill
    row.spacing = 8
    return row
}
```

Bu satırda dağıtım `fill` olduğundan etiket ile rozet arasında bir görünümün uzaması gerekir. Hangisinin uzayacağı açıkça söylenmediyse çerçeve içsel boyuta ve dirençlere bakar. Rozeti sabit boyutta tutmak için rozetin büyüme direncini yükseltmek yeterlidir.

#### Gizlemek ile Çıkarmak Aynı Şey Değildir

Yığıt görünümünün en kullanışlı davranışı, gizlenen bir alt görünümün yerini kapatmasıdır. `isHidden` değeri `true` yapılan görünüm yerleşimden çıkar ve komşuları boşluğu doldurur. Bu davranış yığıt görünümüne özgüdür; yığıt dışında gizlenen bir görünüm yerini korur.

Çıkarma ise ayrı bir işlemdir. `removeArrangedSubview` çağrısı görünümü yalnız dağıtımdan çıkarır; görünüm hâlâ yığıt görünümünün alt görünümüdür ve çizilmeye devam eder. Ekrandan gerçekten kaldırmak için `removeFromSuperview` gerekir. Bu fark, bu dersin üçüncü ölçümünün konusudur: gizleme ve kaldırma modelde aynı görünürlük değerini yazar, ama boyut ve yerleşim açısından farklı sonuç verir.

```swift
import UIKit

func hide(_ badge: UIView, in row: UIStackView) {
    badge.isHidden = true
}

func remove(_ badge: UIView, from row: UIStackView) {
    row.removeArrangedSubview(badge)
    badge.removeFromSuperview()
}
```

İkinci işlev iki adım atar çünkü yalnız birincisi görünümü ekranda bırakır. Yalnız ikincisi bellekten de çıkarır; görünümü sonra yeniden göstermek gerekirse gizlemek daha ucuzdur.

#### Yığıt Görünümü mü Kısıt mı

Yığıt görünümü her düzen sorununa cevap değildir. Alt görünümlerin tek bir eksen boyunca sıralandığı durumlarda, yani formlar, satırlar, düğme çubukları ve etiket listelerinde, yığıt görünümü onlarca kısıtı tek bir ayara indirir. Görünümler üst üste biniyorsa, bir görünüm başka bir görünümün köşesine göre konumlanıyorsa ya da oranlar birbirine bağlıysa, kısıtlar daha doğrudur. İkisini birleştirmek de yaygındır: yığıt görünümünün kendisi kısıtlarla konumlanır, içindeki sıralama yığıt görünümüne bırakılır.

Bu seçim ölçüm açısından da önemlidir. Yığıt görünümü yerleşimi kendisi çözdüğü için model ile ekran arasındaki eşleşme alt görünümün varlığına ve görünürlüğüne iner. Kısıt tabanlı düzende ise her sabitin ayrı ayrı güncellenmesi gerekir. Aynı ekranı iki yoldan kurmak, bayat kalma ihtimali olan değer sayısını da değiştirir.

#### Sık Yapılan Hatalar

- Yığıt görünümünün alt görünümü yerine doğrudan ekleme yapmak. Yığıt görünümüne doğrudan `addSubview` ile eklenen görünüm dağıtıma katılmaz.
- Gizleme ile kaldırmayı karıştırmak. Rozet modelden kalktığında yalnız `removeArrangedSubview` çağrılırsa rozet ekranda kalır; bu dersin birinci ölçümündeki artık satırı bu durumdur.
- Dağıtım ile hizalamayı karıştırmak. Dikey yığıtta görünümleri yatayda ortalamak isteyen kişi `distribution` değerini değiştirir ve sonuç alamaz.
- İç içe yığıt görünümlerini gereğinden fazla derinleştirmek. Her katman bir kısıt çözümü daha demektir ve hata ayıklamayı zorlaştırır.
- Aralık bırakmayı alt görünümün kendi kısıtlarına yazmak. Yığıt görünümü bunu `spacing` ile yapar; kısıtla da yazılırsa çelişki doğar.

#### Ölçülen Sonucun Yorumu

İlk ölçümde rozet modelde yokken ekranda duruyordu ve satır artık sayıldı. Yığıt görünümü bunu kendiliğinden temizlemez, çünkü kapsayıcı hangi alt görünümün modelle eşleştiğini bilmez. Rozet iki yoldan kalkar: gizlenir ya da çıkarılır. Rozet gizlendiğinde ya da çıkarıldığında satır ölçüm dışı kaldı, çünkü iki tarafta da değer yoktu. Üçüncü ölçümde gizleme ve kaldırma aynı görünürlük değerini yazdı ve ikisi de uyumlu çıktı: ölçü ekseni bir değerin ekranda bulunup bulunmadığıyla ilgilenir, görünümün yerinin kapanıp kapanmadığıyla ilgilenmez. İkinci ölçüm görünürlük alanında başka bir kırılmayı gösterdi: model gizle dediği hâlde ekran gizlemediyse satır bayattır. Üç ölçümü birlikte okuyunca yığıt görünümü ile ilgili tek bir sonuç çıkar: kapsayıcı yerleşimi çözer, ama modelle eşleşmeyi geliştirici sağlar.

Pratikte bu, yığıt görünümü kullanan bir ekranda iki ayrı yoklama gerektirir. Önce modelde olan her öğenin ekranda bir alt görünümü olup olmadığına bakılır; eksik varsa eklenmemiştir. Sonra ekranda olan her alt görünümün modelde karşılığı aranır; karşılığı olmayan, yani artık kalan görünüm gizlenmemiş ya da çıkarılmamıştır. Çıkarma sırasında da dağıtımdan çıkarmak ile görünüm ağacından kaldırmak ayrı adımlardır ve ikisi de yapılmadıkça görünüm ekranda kalır. Yığıt görünümü bu işi bizim yerimize yapmaz, çünkü kapsayıcı hangi alt görünümün hangi model öğesine ait olduğunu bilmez. Bu iki yoklamayı bir yardımcı işleve toplamak, ekranın her güncellenişinde aynı denetimin yapılmasını sağlar ve unutulan durumların sayısını belirgin biçimde azaltır; çünkü kural bir kez yazılır, her ekranda yeniden hatırlanmaz. Ayrıca bu denetim, rozet gibi koşullu görünen öğelerde her güncellemeden sonra çalıştırılırsa bayat ve artık satırlar ilk ortaya çıktıkları anda görünür.

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

Bir alt görünümü yığıttan iki yolla çıkarabiliriz: gizleyebiliriz ya da yığıttan kaldırabiliriz. İkisinin düzende bıraktığı boşluk aynı olmayabilir; gizlenen görünüm yığıtın dizilimine yer ayırmasa da hâlâ ağaçtadır, kaldırılan ise ağaçtan çıkmıştır. **Gizli alt görünüm yığıt düzeninden düşer**; bu, düzenin bir sonucudur ve model onu ölçmez.

Model yalnız görünürlüğü alan olarak kurar. İki yol aynı alana aynı değeri yazar, bu yüzden boyut farkı hakkında bir iddiada bulunulmaz.

```swift
// main.swift — gizleme ve kaldirma ayni gorunurluk degerini yazar, boyut farki olculmez
let model = ["badgeVisible": "hayir"]
let hiddenRow = Screen("HiddenRow")
let removedRow = Screen("RemovedRow")

hiddenRow.set("badgeVisible", "hayir")
removedRow.set("badgeVisible", "hayir")

let hiddenResult = check("gizlendi", model: model, screen: hiddenRow, keys: ["badgeVisible"])
let removedResult = check("kaldirildi", model: model, screen: removedRow, keys: ["badgeVisible"])

countVerdicts(hiddenResult + removedResult)
```

```
gizlendi | HiddenRow.badgeVisible: model=hayir ekran=hayir -> uyumlu
kaldirildi | RemovedRow.badgeVisible: model=hayir ekran=hayir -> uyumlu
uyumlu: 2
bayat: 0
eksik: 0
artik: 0
```

İki satır birebir aynı sonuç: uyumlu. Gizlemeyle kaldırmanın düzende bıraktığı fark çıktıya girmedi, çünkü modelde bir boyut alanı yok. Bu bir sınırdır: model alt görünümün düzenden düşüp düşmediğini söylemez, görünürlük alanının ekranda yazılıp yazılmadığını söyler.

**DZ3.** Bu ölçüm iki ekranın aynı değeri taşımasını sınar. Yığıt görünümünün boşluğu nasıl kapattığına dair bir sayı yazılmadı.

#### Özet

- Yığıt görünümü alt görünümleri dizer; gizlenen alt görünüm düzenden düşer ve gizlemeyi kod yazar.
- Modelden kalkan bir alan ekranda gizlenmediyse sonuç artıktır.
- İki tarafta da olmayan alan ölçüm dışıdır ve hiçbir sayıya girmez.
- Görünürlük alan olarak tutulduğunda atlanan gizleme bayat verir, alan tümüyle kalktığında artık.
- Gizlemek ile kaldırmak modelde aynı görünürlük değerini yazar; boyut farkı ölçülmez.
- Bu dersin uyumu: **artık**.

#### Sonraki Adım

Yığıt görünümleri alt görünümleri bir kez dizer. Tablo görünümü ise aynı hücreyi çok kez kullanır ve kullanılan hücre önceki satırın izini taşıyabilir. Sıradaki derste hücrenin yeniden kullanımı ve eski satırdan kalan değer ölçülecek.

### Tablo Görünümleri

Source: https://academia.sh/tr/kurslar/uikit/tablo-gorunumleri

Yeniden kullanılan bir hücrede önceki satırın ekinin kalmasının artık, modeli değişen satırın yeniden yüklenmemesinin bayat sayıldığının ve hangi hücrenin geri geldiğinin ölçülmediğinin ölçümü.

Önceki derste, Yığıt Görünümleri, ekranda bir şeyi kaldırmanın onu gizlemek olduğunu ve gizlemeyi kodun yazması gerektiğini ölçtük. **Tablo görünümü** (table view) aynı yükün başka bir biçimini taşır. Uzun bir listede her satır için ayrı bir görünüm üretilmez; ekrandan çıkan **hücre** (cell) başka bir satır için **yeniden kullanılır**. Hücre temizlenmeden geri verilirse, bir önceki satırdan kalan her şey yeni satırın üzerinde durur.

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

```swift
// Sync.swift — ekranda gosterilen her alani modelin o anki degeriyle karsilastirir
final class Screen {
    let name: String
    private(set) var shown: [String: String] = [:]

    init(_ name: String) { self.name = name }

    func set(_ key: String, _ value: String) { shown[key] = value }
    func clear(_ key: String) { shown[key] = nil }
}

func verdict(model: String?, shown: String?) -> String {
    switch (model, shown) {
    case (nil, nil): return "olcum-disi"
    case (nil, _?): return "artik"
    case (_?, nil): return "eksik"
    case let (wanted?, displayed?): return wanted == displayed ? "uyumlu" : "bayat"
    }
}

func check(_ label: String, model: [String: String], screen: Screen, keys: [String]) -> [String] {
    keys.map { key in
        let result = verdict(model: model[key], shown: screen.shown[key])
        print("\(label) | \(screen.name).\(key): model=\(model[key] ?? "-") ekran=\(screen.shown[key] ?? "-") -> \(result)")
        return result
    }
}

func countVerdicts(_ verdicts: [String]) {
    for name in ["uyumlu", "bayat", "eksik", "artik"] {
        print("\(name): \(verdicts.filter { $0 == name }.count)")
    }
}
```

Çekirdek, ekranda gösterilen her alanı modelin o anki değeriyle karşılaştırır. Bir hücre burada bir `Screen`'dir ve satırdan satıra aynı `Screen` nesnesine yeniden yazılır; model, kodun ekrana ne yazdığını kurar, çerçevenin kendisi değildir.

#### Yeniden Kullanılan Hücre

Bir kişi listesi düşünelim. Her satır bir ad gösterir; favori kişilerin yanında ayrıca bir not görünür. Tablo görünümü hücreyi **yeniden kullanım tanımlayıcısıyla** (reuse identifier) ister. Hücre yeni üretilmiş de olabilir, daha önce kullanılmış da.

```swift
import UIKit

final class ContactCell: UITableViewCell {
    func configure(name: String, note: String?) {
        textLabel?.text = name
        if let note {
            detailTextLabel?.text = note
        }
    }
}

func cellForRow(_ tableView: UITableView, at indexPath: IndexPath, contact: Contact) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell", for: indexPath) as! ContactCell
    cell.configure(name: contact.name, note: contact.note)
    return cell
}
```

`configure(name:note:)` notu yalnız varsa yazar; yoksa alanı olduğu gibi bırakır. Bu yüzden notsuz bir satır, notlu bir satırın hücresini devralırsa eski not ekranda kalır. Ölçümde üç adım vardır: ilk satır yazılır, hücre ikinci satır için yeniden kullanılır, sonra hücre temizlenir.

```swift
// main.swift — yeniden kullanilan hucrede onceki satirin notu kalirsa artik, temizlenirse olcum-disi
let cell = Screen("ContactCell")
let rowOne = ["name": "Ayse", "note": "Favori"]
let rowTwo = ["name": "Mehmet"]

cell.set("name", "Ayse")
cell.set("note", "Favori")
let first = check("satir-bir", model: rowOne, screen: cell, keys: ["name", "note"])

cell.set("name", "Mehmet")
let reused = check("yeniden-kullanim", model: rowTwo, screen: cell, keys: ["name", "note"])

cell.clear("note")
let cleaned = check("hazirlik-temizledi", model: rowTwo, screen: cell, keys: ["name", "note"])

countVerdicts(first + reused + cleaned)
```

```
satir-bir | ContactCell.name: model=Ayse ekran=Ayse -> uyumlu
satir-bir | ContactCell.note: model=Favori ekran=Favori -> uyumlu
yeniden-kullanim | ContactCell.name: model=Mehmet ekran=Mehmet -> uyumlu
yeniden-kullanim | ContactCell.note: model=- ekran=Favori -> artik
hazirlik-temizledi | ContactCell.name: model=Mehmet ekran=Mehmet -> uyumlu
hazirlik-temizledi | ContactCell.note: model=- ekran=- -> olcum-disi
uyumlu: 4
bayat: 0
eksik: 0
artik: 1
```

`satir-bir` adımında iki alan uyumlu. `yeniden-kullanim` adımında `name` uyumlu, çünkü ad yeniden yazıldı; `note` için model `-`, ekran `Favori` ve sonuç `artik`. Yeni satırın modelinde not yok, ama hücre önceki satırın notunu taşıyor. `hazirlik-temizledi` adımında alan temizlenince satır `olcum-disi` basar ve sayılmaz.

Sayım altı satırdan beşini kapsar: dört uyumlu, bir artık. Hücrenin yeniden kullanıldığı adım, artığın doğduğu tek yerdir.

**DZ1.** Bu ölçüm iki satırlık bir sıra için. Hücrenin her yeniden kullanımda temizlenmesi gerektiği gibi bir genel kural iddia edilmedi; ölçülen, kodun ekrana ne bıraktığıdır.

KAYNAK tarafta temizliği hücrenin kendisi yapabilir. Tablo görünümü, hücreyi kuyruktan geri vermeden hemen önce onun `prepareForReuse()` yöntemini çağırır; bu yöntemi yeniden tanımlayarak içerikle ilgisiz durum sıfırlanır, içerik ise `cellForRowAt` içinde her seferinde yeniden yazılır.

```swift
import UIKit

extension ContactCell {
    override func prepareForReuse() {
        super.prepareForReuse()
        detailTextLabel?.text = nil
    }
}
```

Önceki ölçümün `hazirlik-temizledi` adımı bu temizliğin sonucunu temsil eder: not alanı boşalır ve model de not taşımadığı için artık ortadan kalkar.

#### Model Değişir, Satır Yenilenmez

Hücrenin yanlış kalması tek sebep değildir. Bazen hücre doğru yapılandırılmıştır ve model sonradan değişir. Tablo görünümü, modelin değiştiğini kendiliğinden bilmez; satırın yeniden yüklenmesi çağrılmalıdır.

```swift
import UIKit

func contactRenamed(_ tableView: UITableView, row: Int) {
    tableView.reloadRows(at: [IndexPath(row: row, section: 0)], with: .automatic)
}
```

Burada `reloadRows(at:with:)` çağrılırsa tablo ilgili satırın hücresini yeniden ister ve `configure` bir kez daha koşar. Çağrılmazsa hücre eski adı göstermeye devam eder. Ölçüm bu iki durumu ayrı adımlar olarak kurar.

```swift
// main.swift — model satir adini degistirir, satir yeniden yuklenmedikce bayat kalir
let cell = Screen("ContactCell")
var model = ["name": "Mehmet"]

cell.set("name", "Mehmet")
let before = check("satir-gosteriliyor", model: model, screen: cell, keys: ["name"])

model["name"] = "Mehmet Ali"
let stale = check("model-degisti", model: model, screen: cell, keys: ["name"])

cell.set("name", "Mehmet Ali")
let reloaded = check("satir-yenilendi", model: model, screen: cell, keys: ["name"])

countVerdicts(before + stale + reloaded)
```

```
satir-gosteriliyor | ContactCell.name: model=Mehmet ekran=Mehmet -> uyumlu
model-degisti | ContactCell.name: model=Mehmet Ali ekran=Mehmet -> bayat
satir-yenilendi | ContactCell.name: model=Mehmet Ali ekran=Mehmet Ali -> uyumlu
uyumlu: 2
bayat: 1
eksik: 0
artik: 0
```

Birinci adımda uyumlu, ikinci adımda model `Mehmet Ali`, ekran `Mehmet` ve sonuç `bayat`. Üçüncü adımda satır yeniden yüklendi ve uyumlu. Sayım iki uyumlu, bir bayat. Yeniden kullanım artık üretti, yeniden yükleme atlandığında ise bayat çıktı: aynı listede iki farklı kopma, iki farklı kategori.

**DZ2.** Bu ölçüm tek bir hücreyi ve tek bir alanı kapsar. Aynı anda birden çok satırın değişmesi ölçülmedi.

#### Hücre Kaydı ve Yeniden Kullanım

Tablo görünümü yüzlerce satır gösterebilir, ama aynı anda yalnız ekrana sığan kadar hücre kurar. Ekrandan çıkan hücre atılmaz; bir havuza girer ve yeni satır için tekrar kullanılır. Bu yüzden hücre iki adımla elde edilir. Önce hücre sınıfı **yeniden kullanım tanımlayıcısıyla** (reuse identifier) tabloya kaydedilir; sonra veri kaynağı her satırda bu tanımlayıcıyla havuzdan bir hücre ister.

```swift
import UIKit

final class ContactListController: UITableViewController {
    var contacts: [String] = []

    override func viewDidLoad() {
        super.viewDidLoad()
        tableView.register(UITableViewCell.self, forCellReuseIdentifier: "ContactCell")
    }

    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        contacts.count
    }

    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell", for: indexPath)
        cell.textLabel?.text = contacts[indexPath.row]
        return cell
    }
}
```

İki zorunlu veri kaynağı yordamı vardır: kaç satır olduğunu söyleyen yordam ve her satırın hücresini kuran yordam. İkincisi her seferinde çağrıldığı için hücrenin bütün alanlarını yazmak zorundadır. Havuzdan gelen hücre önceki satırdan kalan değerleri taşır; bir alan yazılmazsa eski değer görünür.

#### Hücrenin Hazırlanması

Havuza dönen hücre kendini temizleme fırsatı bulur. Hücre sınıfı `prepareForReuse` yordamını yeniden tanımlayarak içerikle ilgisiz durumu (seçim, saydamlık, düzenleme kipi) sıfırlayabilir. İçeriğin kendisi ise her zaman `cellForRowAt` içinde yeniden yazılır; satırın artığını silmenin güvenli yeri orasıdır.

```swift
import UIKit

final class NoteCell: UITableViewCell {
    let noteLabel = UILabel()

    override func prepareForReuse() {
        super.prepareForReuse()
        noteLabel.text = nil
        accessoryType = .none
    }
}
```

Burada iki alan boşaltılmıştır: not metni ve ek simge. Bir alan atlanırsa onun değeri bir sonraki satırda belirir. Hücre temizlenmesi ile veri kaynağındaki atama birbirini tamamlar; ikisinden yalnız birine güvenmek yetersizdir.

#### Veri Değişince Satır Yenilenmez

Tablo görünümü, veri kaynağının içeriğini kendiliğinden izlemez. Dizi değiştiğinde ekran aynı kalır, ta ki tabloya haber verilene kadar. Haber vermenin iki yolu vardır: `reloadData` bütün tabloyu yeniden yükler, `reloadRows(at:with:)` ise yalnız belirtilen satırları. Tek bir satırın adı değiştiyse ikincisi daha ucuzdur ve kaydırma konumunu korur. Satır eklemek ya da silmek ise `insertRows` ve `deleteRows` ile yapılır; bunları veri dizisini aynı anda değiştirmeden çağırmak çalışma anında bir tutarsızlık hatasına yol açar.

#### Tablo Görünümü mü Koleksiyon Görünümü mü

Tablo görünümü tek sütunlu, düşey kayan listeler için tasarlanmıştır ve başlık, alt bilgi, düzenleme kipi ve kaydırarak silme gibi liste davranışlarını hazır getirir. Bir ayar uygulamasındaki satırlar, kişi listesi ya da sipariş geçmişi bunun tipik örnekleridir. Öğeler birden çok sütuna yayılıyorsa, yatay kayıyorsa ya da satır biçimi öğeden öğeye değişiyorsa koleksiyon görünümü daha uygundur. Koleksiyon görünümü liste biçimini de destekler, bu yüzden yeni kurulan ekranlarda ikisi arasındaki fark giderek azalmıştır; ama tablo görünümü hâlâ basit listeler için daha az kod ister.

Yeniden kullanım kavramı ikisinde de aynıdır. Hücre havuzdan gelir, bu yüzden hücreyi yapılandıran kodun her alanı yazması gerekir. Bu dersin artık satırları, hangi görünümü seçtiğinizden bağımsız olarak karşınıza çıkacaktır.

#### Sık Yapılan Hatalar

- Hücreyi kaydetmeden havuzdan istemek. Kayıtsız tanımlayıcıyla yapılan istek çalışma anında uygulamayı durdurur.
- Hücrenin her alanını yazmamak. Havuzdan gelen hücre eski satırın değerlerini taşır; bu dersin ilk ölçümündeki artık satırı budur.
- Hücrede kalıcı durum tutmak. Hücre satırın değil, o anki ekranın parçasıdır; seçili olma gibi durum veri kaynağında saklanmalıdır.
- Hücrenin içinde ağır iş yapmak. Kaydırma sırasında her yeni satır `cellForRowAt` çağırır ve uzun süren iş akıcılığı bozar.
- Veri dizisini değiştirip tabloya haber vermemek. Satırın adı değişir, ama ekran eski adı göstermeye devam eder; ikinci ölçümün bayat sonucu budur.

#### Ölçülen Sonucun Yorumu

Üç ölçümün ortak noktası, hücrenin ve verinin ayrı yaşamasıdır. Birinci ölçümde hücre önceki satırın notunu taşıdı ve model o satırda not tutmadığı için sonuç artık oldu. Hücre temizlenince satır ölçüm dışı kaldı, çünkü iki tarafta da değer yoktu. İkinci ölçümde model değişti ama tabloya haber verilmedi, ekran eski kaldı ve sonuç bayat oldu. Üçüncü ölçümde ise hangi hücrenin geri geldiği ölçülmedi: aynı kalıntı aynı sonucu verir. Bu, yeniden kullanımın sessiz bir davranış olduğunu gösterir. Hata yoktur, uyarı yoktur; yalnız eski bir değer vardır ve onu bulmanın tek yolu modelle karşılaştırmaktır.

Burada pratik bir kural çıkarılabilir: hücreyi yapılandıran kod, bir alanı yazmıyorsa o alanı boşaltmak zorundadır. Alanı atlayan her satır, havuzdan gelen hücrenin önceki değerini yeniden gösterme riski taşır ve bu risk yalnız kaydırma sırasında, belirli bir satır sırasında ortaya çıktığı için testte kaçabilir.

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

Yeniden kullanımın bir de sayısal yüzü vardır: tablo görünümü kaç hücre tutar, hangi hücre hangi satıra geri döner. Bu dersin ölçüsü bunları kapsamaz. **Kuyruk boyutu ve hangi hücrenin geri geldiği ölçülmez.** Model yalnız bir hücrenin iki yapılandırmasını sırayla kurar ve son yazılan değerlere bakar.

Bunu göstermek için aynı kalıntıyı iki farklı hücrede kuralım: biri aynı hücre, biri başka bir hücre olsun. Kural için ikisi arasında fark yoktur.

```swift
// main.swift — hangi hucrenin geri geldigi olculmez, ayni kalinti ayni sonucu verir
let model = ["name": "Mehmet"]
let sameCell = Screen("SameCell")
let otherCell = Screen("OtherCell")

sameCell.set("name", "Mehmet")
sameCell.set("note", "Favori")
otherCell.set("name", "Mehmet")
otherCell.set("note", "Favori")

let sameResult = check("ayni-hucre", model: model, screen: sameCell, keys: ["name", "note"])
let otherResult = check("baska-hucre", model: model, screen: otherCell, keys: ["name", "note"])

countVerdicts(sameResult + otherResult)
```

```
ayni-hucre | SameCell.name: model=Mehmet ekran=Mehmet -> uyumlu
ayni-hucre | SameCell.note: model=- ekran=Favori -> artik
baska-hucre | OtherCell.name: model=Mehmet ekran=Mehmet -> uyumlu
baska-hucre | OtherCell.note: model=- ekran=Favori -> artik
uyumlu: 2
bayat: 0
eksik: 0
artik: 2
```

İki hücre için sonuç birebir aynı: ad uyumlu, not artık. Sayım iki uyumlu, iki artık. Kalıntının hangi hücreden geldiği çıktıya girmedi, çünkü modelde hücrenin kimliğini taşıyan bir alan yok. Bu bir sınırdır: model, ekranın son durumunu karşılaştırır, o duruma hangi hücrenin yol açtığını değil.

**DZ3.** Bu ölçüm iki ekran nesnesinin aynı değerleri taşımasını sınar. Tablo görünümünün kaç hücreyi kuyrukta tuttuğuna dair bir sayı yazılmadı.

#### Özet

- Tablo görünümü hücreyi yeniden kullanım tanımlayıcısıyla geri verir; geri gelen hücre önceki satırın değerlerini taşıyabilir.
- Yeni satırın modelinde olmayan ama hücrede kalan değer artıktır; hücre hazırlanırken temizlenirse ölçüm dışına düşer.
- Modeli değişen satır yeniden yüklenmezse ekran bayat kalır.
- Yeniden kullanım artık, atlanan yeniden yükleme bayat üretir; iki ayrı kopma iki ayrı kategoridedir.
- Kuyruk boyutu ve hangi hücrenin geri geldiği ölçülmez; model son yazılan değerleri karşılaştırır.
- Bu dersin uyumu: **artık**.

#### Sonraki Adım

Tablo görünümü tek sütunlu bir listedir. Sıradaki derste koleksiyon görünümü öğeleri iki boyutta dizer ve veriye öğe eklendiğinde ekranın bunu öğrenmesi gerekir. O derste yeni bir uyum görülecek: eksik.

### Koleksiyon Görünümleri

Source: https://academia.sh/tr/kurslar/uikit/koleksiyon-gorunumleri

Veri kaynağına eklenen bir öğenin koleksiyon görünümüne bildirilmediği durumun eksik, anlık görüntü uygulandığında uyumlu sayıldığının ve konum hesabının ölçülmediğinin ölçümü.

Önceki derste, Tablo Görünümleri, bir hücrenin yeniden kullanıldığında önceki satırın izini taşıyabildiğini ve model değiştiğinde satırın ayrıca yeniden yüklenmesi gerektiğini ölçtük. **Koleksiyon görünümü** (collection view) aynı fikri iki boyuta taşır: öğeler bir ızgarada ya da başka bir esnek düzende dizilir. Bu dersin ölçüsü öğelerin sayısına bakar. Veri kaynağına bir öğe eklendiğinde ekranın onu görüp görmediği, ayrı bir bildirimle belirlenir.

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

```swift
// Sync.swift — ekranda gosterilen her alani modelin o anki degeriyle karsilastirir
final class Screen {
    let name: String
    private(set) var shown: [String: String] = [:]

    init(_ name: String) { self.name = name }

    func set(_ key: String, _ value: String) { shown[key] = value }
    func clear(_ key: String) { shown[key] = nil }
}

func verdict(model: String?, shown: String?) -> String {
    switch (model, shown) {
    case (nil, nil): return "olcum-disi"
    case (nil, _?): return "artik"
    case (_?, nil): return "eksik"
    case let (wanted?, displayed?): return wanted == displayed ? "uyumlu" : "bayat"
    }
}

func check(_ label: String, model: [String: String], screen: Screen, keys: [String]) -> [String] {
    keys.map { key in
        let result = verdict(model: model[key], shown: screen.shown[key])
        print("\(label) | \(screen.name).\(key): model=\(model[key] ?? "-") ekran=\(screen.shown[key] ?? "-") -> \(result)")
        return result
    }
}

func countVerdicts(_ verdicts: [String]) {
    for name in ["uyumlu", "bayat", "eksik", "artik"] {
        print("\(name): \(verdicts.filter { $0 == name }.count)")
    }
}
```

Çekirdek, ekranda gösterilen her alanı modelin o anki değeriyle karşılaştırır. Bu derste her öğe bir anahtardır: öğenin kimliği anahtar, başlığı değerdir. Model, kodun ekrana ne yazdığını kurar; çerçevenin kendisi değildir.

#### Eklenen Öğe Ekrana Ulaşmayabilir

Bir görev panosu düşünelim. Pano, görevleri bir koleksiyon görünümünde gösterir. Görev dizisi **veri kaynağında** (data source) durur; koleksiyon görünümü öğe sayısını ve hücreleri oradan ister. Diziye bir görev eklendiğinde görünüm bunu kendiliğinden bilmez.

```swift
import UIKit

final class BoardController: UIViewController, UICollectionViewDataSource {
    var tasks = ["Rapor", "Test"]
    @IBOutlet var collectionView: UICollectionView!

    func addTask(_ title: String) {
        tasks.append(title)
        collectionView.insertItems(at: [IndexPath(item: tasks.count - 1, section: 0)])
    }
}
```

`addTask(_:)` diziye ekler ve ardından `insertItems(at:)` çağırır. Bu çağrı olmazsa koleksiyon görünümü eski öğe sayısıyla çalışmaya devam eder. Ölçümde üç adım vardır: pano açılır, model üçüncü görevi alır, ekran bunu yazar.

```swift
// main.swift — veri kaynagina eklenen oge bildirilmedikce eksik kalir
let board = Screen("TaskBoard")
var model = ["task1": "Rapor", "task2": "Test"]

board.set("task1", "Rapor")
board.set("task2", "Test")
let opened = check("acilis", model: model, screen: board, keys: ["task1", "task2", "task3"])

model["task3"] = "Yayin"
let added = check("oge-eklendi", model: model, screen: board, keys: ["task1", "task2", "task3"])

board.set("task3", "Yayin")
let notified = check("ekleme-bildirildi", model: model, screen: board, keys: ["task1", "task2", "task3"])

countVerdicts(opened + added + notified)
```

```
acilis | TaskBoard.task1: model=Rapor ekran=Rapor -> uyumlu
acilis | TaskBoard.task2: model=Test ekran=Test -> uyumlu
acilis | TaskBoard.task3: model=- ekran=- -> olcum-disi
oge-eklendi | TaskBoard.task1: model=Rapor ekran=Rapor -> uyumlu
oge-eklendi | TaskBoard.task2: model=Test ekran=Test -> uyumlu
oge-eklendi | TaskBoard.task3: model=Yayin ekran=- -> eksik
ekleme-bildirildi | TaskBoard.task1: model=Rapor ekran=Rapor -> uyumlu
ekleme-bildirildi | TaskBoard.task2: model=Test ekran=Test -> uyumlu
ekleme-bildirildi | TaskBoard.task3: model=Yayin ekran=Yayin -> uyumlu
uyumlu: 7
bayat: 0
eksik: 1
artik: 0
```

`acilis` adımında `task3` her iki tarafta da yok ve `olcum-disi` basar; öteki ikisi uyumlu. `oge-eklendi` adımında `task3` için model `Yayin`, ekran `-` ve sonuç `eksik`: veri kaynağı öğeyi biliyor, ekran bilmiyor. `ekleme-bildirildi` adımında öğe yazılınca üç satır da uyumlu.

Sayım dokuz satırın sekizini kapsar: yedi uyumlu, bir eksik. Eksik burada bir öğe düzeyinde doğuyor: modelde olan, ekranda hiç olmayan bir görev.

**DZ1.** Bu ölçüm üç görevle yapıldı ve eklenen öğe sonda duruyor. Öğenin dizinin ortasına eklenmesi ölçülmedi.

#### Anlık Görüntü Her Şeyi Birlikte Yazar

Tek tek `insertItems(at:)` çağırmak, silme ve taşıma da eklendiğinde dizinleri elle eşlemeyi gerektirir. Bir **anlık görüntü** (snapshot) bu yükü kaldırır: veri kaynağına tüm öğelerin yeni listesi verilir, görünüm farkı kendisi bulur.

```swift
import UIKit

func applyBoard(_ tasks: [String], to dataSource: UICollectionViewDiffableDataSource<Int, String>) {
    var snapshot = NSDiffableDataSourceSnapshot<Int, String>()
    snapshot.appendSections([0])
    snapshot.appendItems(tasks)
    dataSource.apply(snapshot)
}
```

Burada ekleme ve silme tek çağrıyla ekrana gider. Ölçümde model aynı anda bir görevi eklesin, bir görevi kaldırsın. Anlık görüntü yazılana kadar ikisi de ekrana ulaşmamıştır. Yardımcı işlev, ekrandaki öğelerin tümünü modele göre yeniden yazar.

```swift
// main.swift — anlik goruntu eklemeyi ve kaldirmayi birlikte uygular
func applySnapshot(_ items: [String: String], to screen: Screen) {
    for key in screen.shown.keys.sorted() where items[key] == nil {
        screen.clear(key)
    }
    for (key, value) in items.sorted(by: { $0.key < $1.key }) {
        screen.set(key, value)
    }
}

let board = Screen("TaskBoard")
let model = ["task2": "Test", "task3": "Yayin"]

board.set("task1", "Rapor")
board.set("task2", "Test")
let before = check("anlik-goruntu-oncesi", model: model, screen: board, keys: ["task1", "task2", "task3"])

applySnapshot(model, to: board)
let after = check("anlik-goruntu-sonrasi", model: model, screen: board, keys: ["task1", "task2", "task3"])

countVerdicts(before + after)
```

```
anlik-goruntu-oncesi | TaskBoard.task1: model=- ekran=Rapor -> artik
anlik-goruntu-oncesi | TaskBoard.task2: model=Test ekran=Test -> uyumlu
anlik-goruntu-oncesi | TaskBoard.task3: model=Yayin ekran=- -> eksik
anlik-goruntu-sonrasi | TaskBoard.task1: model=- ekran=- -> olcum-disi
anlik-goruntu-sonrasi | TaskBoard.task2: model=Test ekran=Test -> uyumlu
anlik-goruntu-sonrasi | TaskBoard.task3: model=Yayin ekran=Yayin -> uyumlu
uyumlu: 3
bayat: 0
eksik: 1
artik: 1
```

Öncesinde `task1` için model `-` ve ekran `Rapor`, sonuç `artik`; `task3` için model `Yayin`, ekran `-`, sonuç `eksik`; `task2` uyumlu. Sonrasında `task1` iki tarafta da yok, `olcum-disi`; `task3` ve `task2` uyumlu. Sayım üç uyumlu, bir eksik, bir artık. Aynı anlık görüntü iki farklı kopmayı birlikte kapattı.

**DZ2.** Bu ölçüm `applySnapshot` yardımcısını modelin ekran tarafı sayar; gerçek `apply` çağrısının animasyonlu mu animasyonsuz mu koştuğu ölçülmedi.

#### Veri Kaynağı ve Anlık Görüntü

Koleksiyon görünümü tablo görünümünden daha esnektir: satır yerine **öğe** (item) gösterir ve öğeleri ızgara, sütun ya da yazarın tasarladığı herhangi bir düzende yerleştirebilir. Verinin ekrana nasıl aktarılacağı ise iki yoldan biriyle yapılır. Birincisi tablodaki gibi sayım ve hücre yordamlarını yazmaktır; ikincisi **farklılık veren veri kaynağı** (diffable data source) kurmaktır.

Farklılık veren veri kaynağı, ekranda ne olacağını bir **anlık görüntü** (snapshot) ile söyler. Anlık görüntü bölümleri ve her bölümdeki öğe kimliklerini taşıyan bir değerdir. Yeni bir anlık görüntü uygulandığında veri kaynağı eskisiyle yenisi arasındaki farkı hesaplar ve yalnız değişen öğeleri ekler, siler ya da taşır. Böylece `reloadData` çağırmak ya da satır dizinlerini elle hesaplamak gerekmez.

```swift
import UIKit

enum BoardSection {
    case main
}

final class TaskBoardController: UIViewController {
    var dataSource: UICollectionViewDiffableDataSource<BoardSection, String>!

    func apply(titles: [String]) {
        var snapshot = NSDiffableDataSourceSnapshot<BoardSection, String>()
        snapshot.appendSections([.main])
        snapshot.appendItems(titles)
        dataSource.apply(snapshot, animatingDifferences: true)
    }
}
```

İki tür parametre vardır: bölüm kimliği ve öğe kimliği. İkisi de `Hashable` olmalıdır. Kimlik, öğenin değerini değil **kimliğini** taşır; bu yüzden iki öğenin aynı kimliği paylaşması çalışma anında bir hataya yol açar. Kimlik benzersiz değilse anlık görüntü kurulamaz.

#### Hücre Kaydı

Koleksiyon görünümünde hücre kaydı bir kayıt nesnesiyle yapılır. Kayıt nesnesi hücrenin nasıl yapılandırılacağını bir kapanışla bildirir ve veri kaynağı bu nesneyi hücre sağlayıcısında kullanır. Böylece tanımlayıcı dizeler kaybolur; yanlış yazılmış bir tanımlayıcının çalışma anında patlaması sorunu da ortadan kalkar.

```swift
import UIKit

func makeDataSource(for collectionView: UICollectionView) -> UICollectionViewDiffableDataSource<BoardSection, String> {
    let registration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, title in
        var content = cell.defaultContentConfiguration()
        content.text = title
        cell.contentConfiguration = content
    }
    return UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, title in
        collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: title)
    }
}
```

Hücre sağlayıcısı her görünür öğe için çağrılır ve öğe kimliğini alır. Kimlikten içeriğe giden yol kapanışın içindedir; burada kimlik doğrudan başlık olarak kullanılmıştır, ama gerçek uygulamada kimlik bir modelin tanımlayıcısıdır ve içerik ayrı bir kaynaktan okunur.

#### Düzen Ayrı Bir Nesnedir

Koleksiyon görünümünün öğeleri nasıl yerleştirdiği **düzen** (layout) nesnesinde saklıdır. Veri kaynağı ve düzen birbirinden bağımsızdır: aynı anlık görüntü hem ızgara hem liste düzeniyle gösterilebilir. Bu dersin üçüncü ölçümü bunu doğrular; ızgara ve liste panoları aynı kimlikleri taşır ve düzen farkı çıktıya girmez.

#### Düzen Seçimi

Koleksiyon görünümünün düzeni, görünümün ne kadar esnek olacağını belirler. Akış düzeni öğeleri satır satır dizer ve sabit boyutlu ızgaralar için yeterlidir. Birleşik düzen ise bölümlere, gruplara ve öğelere ayrı boyut tarifi verir; karışık biçimli ekranlar, yatay kayan şeritler ve başlıklı bölümler bu düzenle kurulur. Liste düzeni tablo görünümünün davranışlarını koleksiyon görünümüne getirir. Düzen seçimi veri kaynağını etkilemez; aynı anlık görüntü her üç düzenle de gösterilebilir.

Bu ayrım, anlık görüntü yaklaşımının asıl kazancıdır. Veri bir yerde, görünüm biçimi başka bir yerde kalır ve ikisi birbirini bilmez. Ekranın düzenini değiştirmek için veriye dokunmak gerekmez; veriyi değiştirmek için de düzene.

#### Sık Yapılan Hatalar

- Kimliğin değer olarak kullanılması. Öğenin görünen metni değişebilir; kimlik değişmemelidir. Kimlik olarak metni kullanan veri kaynağı, metin değişince öğeyi silinip yeniden eklenmiş sayar.
- Aynı kimliği iki kez eklemek. Anlık görüntü yinelenen kimliği kabul etmez.
- Anlık görüntüyü her seferinde sıfırdan kurup öğeleri yeniden eklemeyi unutmak. Eksik öğe ekrandan kalkar.
- Mevcut öğenin içeriği değişince anlık görüntüyü uygulayıp farkın çıkmasını beklemek. Kimlik aynı kaldığında veri kaynağı farkı görmez; içeriği yenilemek için ayrıca yeniden yükleme istenir.
- Düzen ile veri kaynağını aynı yerde karıştırmak. Düzen bilgisini hücre sağlayıcısına yazmak bakımı zorlaştırır.

#### Ölçülen Sonucun Yorumu

İlk ölçümde üçüncü görev modele girdi ama veri kaynağına bildirilmedi: `oge-eklendi` adımında model `Yayin` iken ekran boştu ve satır eksik çıktı. Ekleme bildirilince aynı satır uyumlu oldu. İkinci ölçümde iki fark birlikte vardı: birinci görev modelden kalkmıştı ama ekranda duruyordu, bu artık satırdı; üçüncü görev ise eksikti. Anlık görüntü uygulandıktan sonra birinci görev iki tarafta da yoktu ve ölçüm dışı kaldı, üçüncü görev uyumlu oldu. Sayım üç uyumlu, bir eksik ve bir artık verdi; sayıma yalnız ilk adımdaki durum girdi. Üçüncü ölçüm düzenin uyumu etkilemediğini gösterdi: ızgara ve liste aynı dört satırı uyumlu verdi, sütun sayısı çıktıya girmedi.

Üç sonuç aynı noktaya çıkar. Farklılık veren veri kaynağı farkı hesaplar, ama farkın girdisi olan anlık görüntüyü her değişiklikte vermek geliştiricinin işidir. Anlık görüntü uygulanmadığı sürece model ile ekran ayrı yaşar: eklenen öğe eksik, silinen öğe artık olarak kalır.

Pratik kural şudur: model ne zaman değişirse, yeni bir anlık görüntü kurulur ve uygulanır. Anlık görüntüyü model değişikliğinin kendisine bağlamak, unutulma ihtimalini azaltır; ayrı bir yerde güncellemek ise ekranı bayat bırakan değişikliklerin başlıca kaynağıdır.

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

Koleksiyon görünümünün özü esnek düzendir: öğeler üç sütunda da dizilebilir, tek sütunda da. Hangi öğenin hangi satıra ve sütuna düştüğü düzenin hesabıdır. **Esnek düzenin konum hesabı ölçülmez**; modelde bir konum anahtarı yoktur, anahtar yalnız öğenin kimliğidir.

İki pano aynı öğeleri taşısın: biri üç sütunlu bir ızgara, öteki tek sütunlu bir liste olarak düşünülsün. Sütun sayısı modelde bir alan değildir, bu yüzden ikisi aynı sonucu verir.

```swift
// main.swift — sutun sayisi modelde yok, iki duzen ayni sonucu verir
let model = ["task1": "Rapor", "task2": "Test"]
let grid = Screen("GridBoard")
let list = Screen("ListBoard")

for (key, value) in model.sorted(by: { $0.key < $1.key }) {
    grid.set(key, value)
    list.set(key, value)
}

let gridResult = check("izgara", model: model, screen: grid, keys: ["task1", "task2"])
let listResult = check("liste", model: model, screen: list, keys: ["task1", "task2"])

countVerdicts(gridResult + listResult)
```

```
izgara | GridBoard.task1: model=Rapor ekran=Rapor -> uyumlu
izgara | GridBoard.task2: model=Test ekran=Test -> uyumlu
liste | ListBoard.task1: model=Rapor ekran=Rapor -> uyumlu
liste | ListBoard.task2: model=Test ekran=Test -> uyumlu
uyumlu: 4
bayat: 0
eksik: 0
artik: 0
```

Dört satırın hepsi uyumlu ve sayım dört uyumlu. Izgara ile liste arasındaki fark çıktıya girmedi. Bu bir sınırdır: model öğenin ekranda olup olmadığını söyler, nerede durduğunu söylemez.

**DZ3.** Bu ölçüm iki panonun aynı anahtar kümesini taşımasını sınar. Öğelerin sütunlara dağılımına dair bir sayı yazılmadı.

#### Özet

- Koleksiyon görünümü öğeleri veri kaynağından alır; eklemenin ayrıca bildirilmesi gerekir.
- Veri kaynağına eklenen ama ekrana yazılmayan öğe eksiktir.
- Anlık görüntü eklemeyi ve kaldırmayı birlikte uygular; önceki artık ve eksik tek adımda kapanır.
- Hiç var olmamış ve iki tarafta da olmayan öğe ölçüm dışıdır.
- Esnek düzenin konum hesabı ölçülmez; model yalnız öğenin ekranda olup olmadığını karşılaştırır.
- Bu dersin uyumu: **eksik**.

#### Sonraki Adım

Tablo ve koleksiyon görünümleri veriyi ve davranışı kendileri tutmaz, başka bir nesneye danışır. Sıradaki derste bu danışma ilişkisi, temsilci kalıbı, ele alınacak. Zayıf tutulan bir bağ boşa düştüğünde ekran bayat kalabilir.

### Temsilci Kalıbı

Source: https://academia.sh/tr/kurslar/uikit/temsilci-kalibi

Zayıf tutulan temsilcinin bağlanmadığında ya da serbest bırakıldığında çağrının boşa düştüğünün ve ekranın bayat kaldığının, bağlanmamayla yöntemin yazmamasının aynı sonucu verdiğinin ölçümü.

Önceki derste, Koleksiyon Görünümleri, bir görünümün veriyi ve davranışı kendisi tutmayıp başka bir nesneye danıştığını gördük. O danışma ilişkisinin adı **temsilci** (delegate) kalıbıdır. Bir nesne, işin bir kısmını bir **protokole** uyan başka bir nesneye devreder: seçim yapıldığında, kayıt bittiğinde ya da bir satıra dokunulduğunda temsilcinin yöntemi çağrılır. Temsilci bağlı değilse çağrı hiçbir yere gitmez, ve bu sessiz bir kopmadır.

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

```swift
// Sync.swift — ekranda gosterilen her alani modelin o anki degeriyle karsilastirir
final class Screen {
    let name: String
    private(set) var shown: [String: String] = [:]

    init(_ name: String) { self.name = name }

    func set(_ key: String, _ value: String) { shown[key] = value }
    func clear(_ key: String) { shown[key] = nil }
}

func verdict(model: String?, shown: String?) -> String {
    switch (model, shown) {
    case (nil, nil): return "olcum-disi"
    case (nil, _?): return "artik"
    case (_?, nil): return "eksik"
    case let (wanted?, displayed?): return wanted == displayed ? "uyumlu" : "bayat"
    }
}

func check(_ label: String, model: [String: String], screen: Screen, keys: [String]) -> [String] {
    keys.map { key in
        let result = verdict(model: model[key], shown: screen.shown[key])
        print("\(label) | \(screen.name).\(key): model=\(model[key] ?? "-") ekran=\(screen.shown[key] ?? "-") -> \(result)")
        return result
    }
}

func countVerdicts(_ verdicts: [String]) {
    for name in ["uyumlu", "bayat", "eksik", "artik"] {
        print("\(name): \(verdicts.filter { $0 == name }.count)")
    }
}
```

Çekirdek, ekranda gösterilen her alanı modelin o anki değeriyle karşılaştırır. Bu derste ayrıca, bir seçici ile onu dinleyen temsilcilerden oluşan saf Swift kodu ölçülür. Bu kod platform çerçevesi içermez, o yüzden gerçekten koşar. Model, kodun ekrana ne yazdığını kurar; çerçevenin kendisi değildir.

#### Zayıf Bağlanan Temsilci

Bir ödeme ekranında teslimat adresini bir seçici verir. Seçici adres seçildiğinde temsilciye haber verir; ekranın görevi bu adresi göstermektir. Temsilci seçicide **zayıf referansla** (weak) tutulur: seçici, ekrana sahip çıkarsa ekran da seçiciye sahip çıkar ve ikisi birbirini serbest bırakmayan bir **tutma döngüsüne** (retain cycle) girer. Zayıf referans bunu engeller, ama bedeli vardır: temsilci serbest kalırsa referans `nil` olur.

```swift
// AddressPicker.swift — adres secici ve onu dinleyen temsilciler
protocol AddressPickerDelegate: AnyObject {
    func picker(_ picker: AddressPicker, didSelect address: String)
}

final class AddressPicker {
    weak var delegate: AddressPickerDelegate?

    func choose(_ address: String) {
        delegate?.picker(self, didSelect: address)
    }
}

final class CheckoutScreen: AddressPickerDelegate {
    let screen: Screen

    init(_ screen: Screen) { self.screen = screen }

    func picker(_ picker: AddressPicker, didSelect address: String) {
        screen.set("address", address)
    }
}

final class SilentCheckout: AddressPickerDelegate {
    func picker(_ picker: AddressPicker, didSelect address: String) {}
}
```

`choose(_:)` temsilciyi isteğe bağlı zincirle çağırır: temsilci `nil` ise hiçbir şey olmaz ve hata da çıkmaz. `CheckoutScreen` seçilen adresi ekrana yazar. `SilentCheckout` ise protokole uyar ama yöntemi boştur. İlk ölçümde seçici önce bağlanmadan kullanılır, sonra bağlanır.

```swift
// main.swift — baglanmayan temsilci ekrani bayat birakir, baglaninca uyumlu olur
let screen = Screen("Checkout")
screen.set("address", "Ankara")
var model = ["address": "Ankara"]

let picker = AddressPicker()
let checkout = CheckoutScreen(screen)
let start = check("baslangic", model: model, screen: screen, keys: ["address"])

model["address"] = "Izmir"
picker.choose("Izmir")
let unwired = check("temsilci-yok", model: model, screen: screen, keys: ["address"])

picker.delegate = checkout
picker.choose("Izmir")
let wired = check("temsilci-bagli", model: model, screen: screen, keys: ["address"])

countVerdicts(start + unwired + wired)
```

```
baslangic | Checkout.address: model=Ankara ekran=Ankara -> uyumlu
temsilci-yok | Checkout.address: model=Izmir ekran=Ankara -> bayat
temsilci-bagli | Checkout.address: model=Izmir ekran=Izmir -> uyumlu
uyumlu: 2
bayat: 1
eksik: 0
artik: 0
```

`baslangic` adımında uyumlu. `temsilci-yok` adımında model `Izmir`, ekran `Ankara` ve sonuç `bayat`: seçici adresi seçti, ama kimseye iletmedi. `temsilci-bagli` adımında aynı çağrı bu kez ekrana ulaştı ve satır uyumlu. Sayım iki uyumlu, bir bayat.

**DZ1.** Bu ölçüm temsilcinin yönteminin eşzamanlı çağrıldığını varsayar. Çağrının başka bir kuyrukta mı koştuğu ölçülmedi.

#### Serbest Kalan Temsilci

Bağlanmamış bir temsilci akla ilk gelen kopmadır. Daha sinsi olanı, bağlanmış temsilcinin serbest kalmasıdır. Ekran kapanır ya da başka bir nesne onu tutmayı bırakır; zayıf referans `nil` olur ve seçici, bağlıymış gibi çağrı yapmaya devam eder. Bu derste `nil` olma bir tahmin değil, gerçekten ölçülür: referans aşağıda iki kez okunur.

```swift
// main.swift — zayif referans temsilci serbest kalinca nil olur, cagri bosa duser
let screen = Screen("Checkout")
screen.set("address", "Ankara")
let model = ["address": "Bursa"]

let picker = AddressPicker()
var handler: CheckoutScreen? = CheckoutScreen(screen)
picker.delegate = handler
print("temsilci bagli: \(picker.delegate != nil)")

handler = nil
print("temsilci bagli: \(picker.delegate != nil)")

picker.choose("Bursa")
let released = check("temsilci-birakildi", model: model, screen: screen, keys: ["address"])

countVerdicts(released)
```

```
temsilci bagli: true
temsilci bagli: false
temsilci-birakildi | Checkout.address: model=Bursa ekran=Ankara -> bayat
uyumlu: 0
bayat: 1
eksik: 0
artik: 0
```

İlk satır `true`, ikinci satır `false`. Temsilci serbest bırakıldığında zayıf referans kendiliğinden `nil` oldu; sonraki `choose` çağrısı hiçbir yere gitmedi. Ekran `Ankara` olarak kaldı, model `Bursa` ve sonuç `bayat`. Sayım bir bayat.

İlk iki satır modelin bir varsayımı değil, Swift'in zayıf referans davranışının gerçek sonucudur. Üçüncü satırdaki bayat ise bu iki satırın sonucudur: çağrı boşa düştüğü için ekrana yazılan bir şey olmadı.

**DZ2.** Bu ölçüm yalnız `handler` değişkenini `nil` yapmanın serbest bırakma olduğunu varsayar. Başka bir güçlü referansın nesneyi tutmaya devam ettiği durum ölçülmedi.

KAYNAK tarafta aynı bağ, bir görünüm denetleyicisinin bir tablo görünümünü kurarken kendini temsilci yapmasıyla kurulur.

```swift
import UIKit

final class CheckoutController: UIViewController, AddressPickerDelegate {
    let picker = AddressPicker()

    override func viewDidLoad() {
        super.viewDidLoad()
        picker.delegate = self
    }
}
```

`picker.delegate = self` satırı unutulursa derleyici uyarmaz; yalnız ekran bayat kalır.

#### Protokol Tasarımı

Temsilci kalıbında bir nesne, kendi adına karar vermesi gereken bir noktada başka bir nesneye sorar ya da haber verir. Soran nesne **temsilcinin** (delegate) kim olduğunu bilmez; yalnız bir **protokole** uyduğunu bilir. Bu sayede sorunun sahibi ile cevabın sahibi birbirinden ayrılır. Hazır görünümlerin çoğu bu kalıpla çalışır: tablo görünümü, seçim denetleyicileri ve metin alanları kendi davranışlarını temsilcilerine bırakır.

İyi bir temsilci protokolü üç özellik taşır. Birincisi, yordam adı olayı bir fiil kalıbıyla anlatır: bir şey olacak (`will`), olmakta (`did`) ya da onay gerekiyor (`should`). İkincisi, ilk parametre olayı gönderen nesnedir; böylece tek bir temsilci birden çok nesneye hizmet edebilir ve hangisinden geldiğini ayırt eder. Üçüncüsü, protokol küçüktür: her yordam bir karar ya da bildirim taşır, protokolün kendisi bir sınıfın bütün davranışını tarif etmez.

```swift
import UIKit

protocol AddressPickerDelegate: AnyObject {
    func addressPicker(_ picker: AddressPicker, didSelect address: String)
    func addressPickerDidCancel(_ picker: AddressPicker)
}
```

Protokol `AnyObject` ile sınırlanmıştır, yani yalnız sınıflar uyabilir. Bunun nedeni bir sonraki başlıktadır.

#### Zayıf Referans

Temsilci tutan nesne, temsilciyi **zayıf referansla** (weak reference) saklar. Nedeni bellek döngüsüdür: ekran seçiciyi tutar, seçici de ekranı temsilci olarak tutarsa iki nesne birbirini bırakmaz ve ikisi de hiç serbest kalmaz. Zayıf referans nesneyi ayakta tutmaz; temsilci başka yerde yok olursa referans kendiliğinden `nil` olur.

```swift
import UIKit

final class AddressPicker {
    weak var delegate: AddressPickerDelegate?

    func choose(_ address: String) {
        delegate?.addressPicker(self, didSelect: address)
    }
}
```

`weak` yalnız sınıf türleri için geçerlidir; protokolün `AnyObject` ile sınırlanması bu yüzden gereklidir. Çağrı, temsilci `nil` ise sessizce atlanır; bu da bu dersin asıl tuzağıdır. Temsilci bağlanmadıysa seçici çalışır, hata vermez ve hiçbir şey olmaz.

#### Bağlanmayan Temsilci

Temsilcinin hiç atanmaması, kalıbın en yaygın hatasıdır. Çağrı iyimser zincirleme ile yapıldığı için çökme yoktur, uyarı yoktur; ekran yalnızca tepki vermez. Bu dersin birinci ve ikinci ölçümleri bunu gösterir. Temsilci atanmadan yapılan seçim ekrana ulaşmaz ve satır bayat kalır. Atandıktan sonra aynı seçim uyumlu sonuç verir. İkinci ölçüm ayrıca zayıf referansı gerçekten sınar: ekran serbest kaldığında seçicinin temsilci bağı `nil` olur ve sonraki seçimler yine kimseye ulaşmaz.

#### Temsilci mi Kapanış mı

Aynı iş çoğu zaman bir kapanışla da yapılabilir. Seçici, seçim tamamlandığında çağıracağı bir kapanışı özellik olarak alabilir. Tek bir olay için bu daha kısadır ve protokol yazmayı gerektirmez. Birden çok ilişkili olay varsa, örneğin seçim, iptal ve doğrulama sorusu, protokol daha okunaklıdır ve gönderen nesne parametre olarak geçtiği için tek temsilci birden çok nesneye hizmet edebilir.

Kapanışlar da bellek döngüsü doğurabilir: kapanış ekranı, ekran seçiciyi tutuyorsa döngü kurulur ve zayıf referans kapanışın içinde ayrıca yazılmalıdır. Temsilci kalıbında bu risk tek bir yerde, `weak` anahtar sözcüğünde toplanır.

#### Sık Yapılan Hatalar

- Temsilciyi güçlü referansla tutmak. Döngü oluşur ve iki nesne de hiç serbest kalmaz.
- Temsilciyi atamayı unutmak. Çağrı sessizce atlanır; çıktı yoktur.
- Protokolü `AnyObject` ile sınırlamamak. Zayıf referans yazılamaz ve derleyici hata verir.
- Gönderen nesneyi parametre olarak geçmemek. Tek temsilci birden çok nesneye hizmet verdiğinde hangisinin çağırdığı bilinmez.
- Temsilcide ağır iş yapmak. Çağrı seçicinin akışında koşar ve uzun işler ekranı bloke eder.
- Temsilciye veri sormak için kullanılan protokol ile bildirim için kullanılan protokolü tek yerde toplamak. Birincisi cevap bekler, ikincisi beklemez; bu ayrım isimlendirmede korunur.

#### Ölçülen Sonucun Yorumu

İlk ölçümde başlangıçta ekran modelle uyumluydu. Model `Izmir` olunca, temsilci bağlı olmadığı için ekran `Ankara` kaldı ve satır bayat çıktı. Temsilci bağlanınca aynı seçim ekrana ulaştı ve satır uyumlu oldu. İkinci ölçümde zayıf referans gerçekten ölçüldü: bağ önce `true`, ekran serbest kalınca `false` basıldı ve sonraki seçim kimseye ulaşmadığı için satır bayat çıktı. Üçüncü ölçümde iki farklı ekran aynı sonuca vardı: biri temsilci bağlanmadığı için, öbürü temsilci bağlı olduğu hâlde seçimi ekrana yazmadığı için bayat kaldı. Çıktı ikisini ayırt etmez.

Sonuç şudur: temsilci kalıbı hata üretmeden başarısız olabilir. Bağın kopması da, bağlı temsilcinin işini yapmaması da aynı sessiz görüntüyü verir ve bunu görmenin tek yolu çıktıyı modelle karşılaştırmaktır.

Pratikte iki yoklama yeterlidir. Birincisi, temsilcinin atandığı yerin tek ve açık olmasıdır: ekran kurulurken seçici oluşturulur ve temsilci hemen atanır, böylece atama arada kalmaz. İkincisi, temsilci çağrısının arkasına sessiz bir yol bırakmamaktır. Çağrının sonucu önemliyse, temsilci yoksa uyarı veren bir koşul eklenir ya da geliştirme sırasında bir kontrol ile durdurulur. Sessiz atlama, kalıbın rahat yanıdır ama ölçümün gösterdiği gibi bir hatanın görünmesini de engeller. Bu iki alışkanlık, ölçümde görülen bayat satırların büyük bölümünü daha yazım aşamasında önler; çünkü bağın kurulduğu yer tek olduğunda, bağın kopması ya da hiç kurulmaması da tek bir yerde aranır ve her ekranda ayrı ayrı izlenmez.

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

Bayat sonucunun iki ayrı sebebi olabilir: temsilci hiç bağlanmamış olabilir ya da bağlıdır ama yöntem bir şey yazmıyordur. Biri bağlantıyı unutmaktır, öteki yöntemin içini boş bırakmaktır. **Kural kimin unuttuğunu okumaz.** Model yalnız ekranda ne yazıldığına bakar.

```swift
// main.swift — baglanmama ile yontemin yazmamasi ayni bayat sonucunu verir
let model = ["address": "Bursa"]
let unwiredScreen = Screen("UnwiredCheckout")
let silentScreen = Screen("SilentCheckout")
unwiredScreen.set("address", "Ankara")
silentScreen.set("address", "Ankara")

let unwiredPicker = AddressPicker()
unwiredPicker.choose("Bursa")

let silentPicker = AddressPicker()
let silentDelegate = SilentCheckout()
silentPicker.delegate = silentDelegate
silentPicker.choose("Bursa")

let unwiredResult = check("baglanmadi", model: model, screen: unwiredScreen, keys: ["address"])
let silentResult = check("yontem-yazmadi", model: model, screen: silentScreen, keys: ["address"])

countVerdicts(unwiredResult + silentResult)
```

```
baglanmadi | UnwiredCheckout.address: model=Bursa ekran=Ankara -> bayat
yontem-yazmadi | SilentCheckout.address: model=Bursa ekran=Ankara -> bayat
uyumlu: 0
bayat: 2
eksik: 0
artik: 0
```

İki satır da `bayat`, sayım iki bayat. Biri hiç çağrılmadı, öteki çağrıldı ve hiçbir şey yazmadı; çıktı ikisini ayırt etmiyor. Bu bir sınırdır: sonucun düzeltilmesi için çağrının nerede koptuğunun ayrıca izlenmesi gerekir, model bunu söylemez.

**DZ3.** Bu ölçüm iki sebebi aynı model değeriyle karşılaştırır. Temsilcinin yöntemi çağrıldığında geçirilen argümanın doğruluğu ayrıca ölçülmedi.

#### Özet

- Temsilci, bir nesnenin işinin bir kısmını bir protokole uyan başka bir nesneye devrettiği kalıptır.
- Temsilci zayıf tutulur ki tutma döngüsü oluşmasın; temsilci serbest kalınca zayıf referans `nil` olur ve çağrı boşa düşer.
- Bağlanmamış ya da serbest bırakılmış temsilci ekranı bayat bırakır; bağlanınca ekran uyumlu olur.
- Zayıf referansın `nil` olması bu derste gerçekten ölçüldü.
- Bağlanmamayla yöntemin hiçbir şey yazmaması aynı bayat sonucunu verir; kural kimin unuttuğunu okumaz.
- Bu dersin uyumu: **bayat**.

#### Sonraki Adım

Temsilci nesneler arası bağı kurar; çizim ise bir görünümün kendi yüzeyine ne yazdığını belirler. Sıradaki derste `draw(_:)` ile çizilen bir görünümün, yeniden çizim istenmediğinde eski değeri göstermeye devam ettiği ölçülecek.

### Çizim ve Katman Animasyonu

Source: https://academia.sh/tr/kurslar/uikit/cizim-ve-katman-animasyonu

draw(_:) ile çizilen bir halkanın yeniden çizim istenmediğinde bayat kaldığının, katman animasyonunda model katmanı yazılmadığında değerin eski haline döndüğünün ve sürenin ölçülmediğinin ölçümü.

Önceki derste, Temsilci Kalıbı, bir bağın kopmasının ekranı sessizce bayat bıraktığını ölçtük. Bu derste kopma, bir görünümün kendi yüzeyiyle ilgili. Bazı görünümler hazır bileşenlerle kurulmaz; kendi içeriğini kendisi **çizer** (drawing). Çizen kod yalnız çağrıldığında koşar, ve çağrılması için bir **yeniden çizim isteği** gerekir. Model değişse de istek gelmediği sürece çizim eski kalır.

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

```swift
// Sync.swift — ekranda gosterilen her alani modelin o anki degeriyle karsilastirir
final class Screen {
    let name: String
    private(set) var shown: [String: String] = [:]

    init(_ name: String) { self.name = name }

    func set(_ key: String, _ value: String) { shown[key] = value }
    func clear(_ key: String) { shown[key] = nil }
}

func verdict(model: String?, shown: String?) -> String {
    switch (model, shown) {
    case (nil, nil): return "olcum-disi"
    case (nil, _?): return "artik"
    case (_?, nil): return "eksik"
    case let (wanted?, displayed?): return wanted == displayed ? "uyumlu" : "bayat"
    }
}

func check(_ label: String, model: [String: String], screen: Screen, keys: [String]) -> [String] {
    keys.map { key in
        let result = verdict(model: model[key], shown: screen.shown[key])
        print("\(label) | \(screen.name).\(key): model=\(model[key] ?? "-") ekran=\(screen.shown[key] ?? "-") -> \(result)")
        return result
    }
}

func countVerdicts(_ verdicts: [String]) {
    for name in ["uyumlu", "bayat", "eksik", "artik"] {
        print("\(name): \(verdicts.filter { $0 == name }.count)")
    }
}
```

Çekirdek, ekranda gösterilen her alanı modelin o anki değeriyle karşılaştırır. Bu derste ekrandaki değer, çizimin en son çizdiği değerdir. Model, kodun ekrana ne yazdığını kurar; çerçevenin kendisi değildir.

#### Çizim Yeniden İstenmedikçe Eski Kalır

Bir yükleme ekranında ilerleme halkası olsun. Halkanın dolu yayı yüzde değerine göre çizilir. Görünüm, çizimini `draw(_:)` yöntemini yeniden tanımlayarak yapar; yöntem, görünüm yeniden çizilmesi gerektiğinde çağrılır.

```swift
import UIKit

final class ProgressRingView: UIView {
    var progress: Int = 0

    override func draw(_ rect: CGRect) {
        let path = UIBezierPath(arcCenter: CGPoint(x: rect.midX, y: rect.midY),
                                radius: rect.width / 2,
                                startAngle: -.pi / 2,
                                endAngle: -.pi / 2 + 2 * .pi * CGFloat(progress) / 100,
                                clockwise: true)
        path.stroke()
    }
}
```

`progress` bir sayıdır ve `draw(_:)` onu okur. Ama `progress` değiştiğinde `draw(_:)` kendiliğinden çağrılmaz. Görünümün yeniden çizilmesi için `setNeedsDisplay()` çağrılmalıdır; bu çağrı çerçeveye bir **yeniden çizim isteği** (redraw request) bırakır ve çizim sonraki turda koşar. Ölçümde üç adım vardır: halka çizilir, model yüzdeyi değiştirir, istek yapılır.

```swift
// main.swift — yeniden cizim istenmedikce halka eski degerde kalir
let ring = Screen("ProgressRing")
var model = ["progress": "40"]

ring.set("progress", "40")
let drawn = check("ilk-cizim", model: model, screen: ring, keys: ["progress"])

model["progress"] = "70"
let stale = check("model-degisti", model: model, screen: ring, keys: ["progress"])

ring.set("progress", "70")
let redrawn = check("yeniden-cizim-istendi", model: model, screen: ring, keys: ["progress"])

countVerdicts(drawn + stale + redrawn)
```

```
ilk-cizim | ProgressRing.progress: model=40 ekran=40 -> uyumlu
model-degisti | ProgressRing.progress: model=70 ekran=40 -> bayat
yeniden-cizim-istendi | ProgressRing.progress: model=70 ekran=70 -> uyumlu
uyumlu: 2
bayat: 1
eksik: 0
artik: 0
```

`ilk-cizim` adımında uyumlu. `model-degisti` adımında model `70`, ekran `40` ve sonuç `bayat`: halka eski yüzdeyi çizmeye devam ediyor. `yeniden-cizim-istendi` adımında çizim yeniden koştu ve satır uyumlu. Sayım iki uyumlu, bir bayat.

KAYNAK tarafta istek, değerin kendi içinde de verilebilir. Özelliğe bir gözlemci eklenir ve değer her değiştiğinde istek bırakılır.

```swift
import UIKit

extension ProgressRingView {
    func update(to percent: Int) {
        progress = percent
        setNeedsDisplay()
    }
}
```

Bu biçimde `update(to:)` çağıran her yol isteği de bırakmış olur. Burada ölçülen, çizimin kendisi değil, isteğin yazılıp yazılmadığıdır.

**DZ1.** Bu ölçüm yeniden çizim isteğini ekrana yazma adımıyla temsil eder. İsteğin çerçevede bir sonraki turda mı koştuğu ya da iki isteğin birleşip birleşmediği ölçülmedi.

#### Model Katmanı ve Sunum Katmanı

Bir görünümün altında bir **katman** (layer) vardır. Katman animasyonları, bir değerin eskisinden yenisine ara değerlerle geçmesini sağlar. Burada iki katman karşımıza çıkar: **model katmanı** değerin son halini hemen taşır, **sunum katmanı** ise ekranda o an görünen ara değeri gösterir. Animasyon eklenir, ama model katmanının değeri yazılmazsa animasyon bitince katman eski değerine döner.

```swift
import UIKit

func fadeIn(_ ring: UIView) {
    let animation = CABasicAnimation(keyPath: "opacity")
    animation.fromValue = 0
    animation.toValue = 1
    animation.duration = 0.4
    ring.layer.add(animation, forKey: "fade")
}

func fadeInAndCommit(_ ring: UIView) {
    ring.layer.opacity = 1
    fadeIn(ring)
}
```

`fadeIn` yalnız animasyonu ekler; model katmanındaki `opacity` eski kalır. `fadeInAndCommit` ise önce model katmanını son değerine yazar, sonra animasyonu ekler. Model burada yalnız model katmanını kurar: animasyonun ortasında ekranda görünen ara değer ölçülmez. İki adımı ölçelim.

```swift
// main.swift — animasyon eklenip model katmani yazilmazsa deger eski haline doner
let layer = Screen("RingLayer")
let model = ["ringOpacity": "1"]

layer.set("ringOpacity", "0")
let added = check("animasyon-eklendi", model: model, screen: layer, keys: ["ringOpacity"])

layer.set("ringOpacity", "1")
let committed = check("model-katmani-yazildi", model: model, screen: layer, keys: ["ringOpacity"])

countVerdicts(added + committed)
```

```
animasyon-eklendi | RingLayer.ringOpacity: model=1 ekran=0 -> bayat
model-katmani-yazildi | RingLayer.ringOpacity: model=1 ekran=1 -> uyumlu
uyumlu: 1
bayat: 1
eksik: 0
artik: 0
```

İlk adımda model `1`, ekran `0` ve sonuç `bayat`: animasyon eklendi, ama katmanın değeri yazılmadı. İkinci adımda model katmanı yazılınca satır uyumlu. Sayım bir uyumlu, bir bayat. Animasyonun ortasında sunum katmanının gösterdiği ara değer bu iki satırın hiçbirinde yok; çıktı yalnız animasyon bittikten sonra katmanda kalan değeri karşılaştırır.

**DZ2.** Bu ölçüm model katmanını tek bir değer olarak temsil eder. Sunum katmanının ara değerleri, animasyonun tamamlanma anı ve animasyonun iptal edilmesi ölçülmedi.

#### Çizimi Çerçeve Çağırır

Elle çizilen görünümde `draw(_:)` yordamını kodunuz hiçbir zaman doğrudan çağırmaz. Çağıran çerçevedir ve bunu görünümün çizime ihtiyacı olduğu işaretlendiğinde, ekran sonraki turu hazırlarken yapar. Bu işaretleme `setNeedsDisplay()` ile yapılır. Çağrı hemen çizmez; yalnız bir istek bırakır. Aynı turda birden çok istek gelirse çerçeve bunları birleştirir ve çizimi tek kez yapar. Bu yüzden bir döngüde yüz kez `setNeedsDisplay()` çağırmak yüz çizim demek değildir.

İsteğin kapsamını daraltmak da mümkündür. `setNeedsDisplay(_:)` bir dikdörtgen alır ve yalnız o bölgenin yeniden çizilmesini ister; `draw(_:)` yordamının aldığı `rect` parametresi o bölgeyi taşır. Büyük bir görünümde yalnız küçük bir parça değiştiyse çizim maliyeti bu sayede düşer.

#### Yerleşim ile Çizim Ayrı İsteklerdir

`setNeedsDisplay()` çizim içindir; boyut ya da konum değişikliği ise `setNeedsLayout()` ile istenir. İkisini karıştırmak sık yapılan bir hatadır. Halkanın yüzdesi değişince çizimin yenilenmesi gerekir, boyutu değişmez; bu yüzden doğru çağrı `setNeedsDisplay()`'dir. Bir kısıtın sabiti değişince ise çizim değil yerleşim gerekir. Görünümün sınırları değiştiğinde çizim gerekip gerekmediğini `contentMode` belirler: varsayılan değer görünümü esnetir, `redraw` değeri ise her sınır değişikliğinde çizimi yeniden ister.

```swift
import UIKit

final class RingContainer: UIView {
    var progress: Int = 0 {
        didSet { setNeedsDisplay() }
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        // Boyut degisti, yerlesim guncellendi; cizim ayri bir istektir.
    }
}
```

`didSet` gözlemcisi, değer her değiştiğinde isteği kendiliğinden bırakır. Böylece isteği unutmak imkansız hale gelir. Gözlemci yerine ayrı bir güncelleme yordamı yazılırsa, o yordamı çağırmayan her yol bayat bırakır.

#### Katman Animasyonunda İki Ağaç

Görünümün altındaki katman iki ayrı katman ağacıyla çalışır. Model katmanı kodun yazdığı değeri taşır. Sunum katmanı ise ekranda o an görünen değeri taşır ve animasyon sırasında ara değerleri gezer. Sunum katmanına `presentation()` yordamıyla erişilir ve değerleri okunabilir, ama yazılmaz.

Animasyon eklendiğinde model katmanı değişmez. Animasyon yalnız sunum katmanını sürer; bittiğinde sunum katmanı model katmanının değerine döner. Bu yüzden animasyonun sonunda kalmasını istediğiniz değer önce model katmanına yazılmalıdır. Animasyonu bitiş durumunda tutmak için `isRemovedOnCompletion` ve `fillMode` ayarları da vardır, ama bunlar model ile sunum arasındaki ayrımı gizler ve sonraki okumada yanlış değer verir. Doğru yol, model katmanını önce yazıp animasyonu başlangıç değerinden başlatmaktır.

#### Çizim mi Hazır Bileşen mi

Elle çizim her ekranda gerekmez. Etiket, görsel ve düğme gibi hazır bileşenler metni, resmi ve dokunmayı kendisi çizer. `draw(_:)` yordamı, ilerleme halkası, grafik ya da özel bir rozet gibi hazır bileşenin karşılamadığı biçimlerde kullanılır. Çizim yolu sabit ve değer yalnız bir sayıysa, katman nesnelerinden oluşan bir çözüm de mümkündür: bir şekil katmanı yolu bir kez kurar ve yüzde değeri yalnız bir özelliği değiştirerek güncellenir. Bu, her değişimde tüm görünümü yeniden çizmekten daha ucuzdur ve katman animasyonuyla doğal biçimde birleşir.

Seçimin ölçü eksenine etkisi şudur: elle çizimde ekranın değeri çizimin son çalıştığı andaki değerdir ve isteğin bırakılması bir yazma adımı sayılır. Katman çözümünde ise ekranın değeri doğrudan katmandadır.

#### Sık Yapılan Hatalar

- `draw(_:)` yordamını doğrudan çağırmak. Geçerli bir çizim bağlamı yoktur ve sonuç çizilmez.
- `setNeedsDisplay()` yerine yerleşim istemek. Yerleşim çizimi yenilemez.
- Animasyon ekleyip model katmanını yazmamak. Animasyon bitince katman eski değerine döner; bu dersin ikinci ölçümündeki bayat satırdır.
- Sunum katmanına yazmak. Değerler sonraki turda silinir.
- `draw(_:)` içinde yeni nesne yaratmak. Her çizimde ayrılan bellek akıcılığı bozar; çizim yolu ve renkler dışarıda hazırlanır.
- Çizim içinde durum değiştirmek. Çizim yalnız okur; durumu yazan çağrı çizimi yeniden istediğinde döngü oluşur.

#### Ölçülen Sonucun Yorumu

İlk ölçümde yüzde `40` iken halka çizildi ve uyumluydu. Model `70` olunca ekran `40` kaldı ve satır bayat oldu; çizim isteği bırakılınca satır uyumlu döndü. Aradaki fark tek bir çağrıdır, ama çıktıda yalnız ekranın son değeri görünür. İkinci ölçümde aynı yapı katman değerinde tekrarlandı: animasyon eklendi, model katmanı yazılmadı ve sonuç bayat oldu. Üçüncü ölçüm sürenin modelde olmadığını gösterdi; iki katman farklı sürelerle aynı son değere vardığı için ikisi de uyumlu çıktı. İki ölçüm birbirini tamamlar. Çizimde yazılması gereken şey bir istektir, animasyonda yazılması gereken şey bir değerdir; ikisi de yazılmazsa ekran modelin gerisinde kalır.

Pratikte iki yoklama yeterlidir. Elle çizilen her değer için, değeri yazan her yolun çizim isteğini de bıraktığından emin olunur; en güvenli biçim değerin kendisine bir gözlemci bağlamaktır. Animasyonlu her değer için ise animasyonu eklemeden önce model katmanına son değerin yazıldığı doğrulanır. İkisi de koda bakarak görülebilir, ama ekranda yalnız test sırasında, belirli bir değişimden sonra ortaya çıkar. Ölçüm bu iki yoklamayı birer satıra çevirir. Bu yaklaşım, ekranın modelin gerisinde kalmasına yol açan iki yaygın nedeni kod incelemesinde yakalanabilir hale getirir; çünkü aranan şey her iki durumda da tek bir satırdır ve satırın yokluğu, ekranda görünmeden önce de fark edilir. Üstelik bu denetimler ekran sayısıyla büyümez; her yeni ekran aynı iki soruyla incelenir ve cevap her seferinde koda bakılarak verilir.

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

Çizim ve animasyon zamanla ilgilidir, ama bu dersin ölçüsü zamanı kapsamaz. **Model katmanı son değeri hemen taşır, sunum katmanı ara değer gösterir; model yalnız model katmanını kurar.** Animasyonun süresi modelde bir alan değildir.

Aynı son değeri hızlı ve yavaş iki animasyonla yazdığımızı düşünelim. KAYNAK parçada süre bir parametreydi; modelde karşılığı yoktur, bu yüzden iki ekran aynı sonucu verir.

```swift
// main.swift — animasyon suresi modelde yok, iki katman ayni sonucu verir
let model = ["ringOpacity": "1"]
let fast = Screen("FastRing")
let slow = Screen("SlowRing")

fast.set("ringOpacity", "1")
slow.set("ringOpacity", "1")

let fastResult = check("kisa-sure", model: model, screen: fast, keys: ["ringOpacity"])
let slowResult = check("uzun-sure", model: model, screen: slow, keys: ["ringOpacity"])

countVerdicts(fastResult + slowResult)
```

```
kisa-sure | FastRing.ringOpacity: model=1 ekran=1 -> uyumlu
uzun-sure | SlowRing.ringOpacity: model=1 ekran=1 -> uyumlu
uyumlu: 2
bayat: 0
eksik: 0
artik: 0
```

İki satır uyumlu, sayım iki uyumlu. Sürenin farkı çıktıya girmedi. Bu bir sınırdır: model, ekrana hangi değerin yazıldığını söyler; o değere ne kadar sürede varıldığını söylemez.

**DZ3.** Bu ölçüm iki katmanın aynı son değeri taşımasını sınar. Kare süresi ya da animasyon eğrisine dair bir sayı yazılmadı.

#### Özet

- Elle çizilen görünüm, çizimini yalnız yeniden çizim isteği geldiğinde yenilenir; istek yazılmazsa ekran bayat kalır.
- İstek yazılınca çizim yeniden koşar ve ekran uyumlu olur.
- Katman animasyonunda model katmanı son değeri hemen taşır, sunum katmanı ara değer gösterir.
- Animasyon eklenip model katmanı yazılmazsa değer eski haline döner ve ekran bayat kalır.
- Animasyonun süresi modelde yoktur; aynı son değer iki süreyle de aynı uyumu verir.
- Bu dersin uyumu: **bayat**.

#### Sonraki Adım

Bu konuda arayüzü buyurgan biçimde kurduk: kısıtlar, hücreler ve çizim kodla yönetildi. Sıradaki konu, köprüleme, buyurgan ve bildirimsel arayüzü aynı ekranda bir araya getirecek. Barındırma denetleyicisi ve sarmalama, iki tarafın birbirine değer aktarmasını sağlayacak; ekranın modelle uyumunu orada da aynı çekirdekle ölçeceğiz.

## Köprüleme

Bildirimsel ve buyurgan arayüzün birbirini sarmalayarak aynı uygulamada çalışması, eski kodun ekran ekran kademeli dönüşümü ve hangi durumda hangi arayüz çerçevesinin seçileceği.

### Bildirimsel ve Buyurgan Arayüz Bir Arada

Source: https://academia.sh/tr/kurslar/uikit/bildirimsel-ve-buyurgan-arayuz-bir-arada

Bildirimsel bir görünümü buyurgan bir ekrana gömerken kök görünüm yeniden atanmadığında ve buyurgan bir görünümü bildirimsel ekrana sararken güncelleme yazılmadığında ekranın modelden geri kaldığının ölçümü.

Önceki iki konuda buyurgan arayüzü tek başına okuduk: ekranda gördüğümüz her değeri kod yazıyordu ve yazmayı unutan koda hiçbir şey hatırlatmıyordu. Önceki kursta (M30/K03) bildirimsel tarafın tersini gördük: değer değişince çerçeve onu okuyan görünümü kendisi yeniden hesaplıyordu. Gerçek uygulamalar nadiren yalnız birinden yapılır. Eski bir ekran bildirimsel bir parça taşır, yeni bir ekran eski bir parçayı içine alır. Bu derste ikisinin dikiş yerine bakıyoruz.

Dikişin iki yönü var. Bildirimsel bir görünüm buyurgan bir ekrana girebilir, buna **gömme** diyeceğiz. Buyurgan bir görünüm bildirimsel bir ekrana girebilir, buna **sarmalama** diyeceğiz. Gömmede girdiyi bildirimsel çerçeve alır ve bunu çerçeve hesaplar; sarmalamada girdiyi buyurgan görünüm alır ve bunu kod yazar. Aynı ekranda yan yana duran iki çerçevenin değer alışverişi ancak bu iki yolla yapılır. Çerçevelerin adları UIKit ve SwiftUI, ama ders adlara bağlı kalmayacak: bildirimsel çerçeve ve buyurgan arayüz çerçevesi diyeceğiz.

Ölçü ekseni konunun başından beri aynıdır: **uyum**. Soru, ekranda gösterilen alanın modelin o anki değeriyle uyuşup uyuşmadığıdır. Kural alan başına dört sonuçtan birini verir: `uyumlu`, `bayat`, `eksik`, `artik`. Köprülemede soru şuna dönüşür: değer iki çerçevenin sınırından geçerken kim taşıyor?

Çekirdek dosya aynıdır.

```swift
// Sync.swift — ekranda gosterilen her alani modelin o anki degeriyle karsilastirir
final class Screen {
    let name: String
    private(set) var shown: [String: String] = [:]

    init(_ name: String) { self.name = name }

    func set(_ key: String, _ value: String) { shown[key] = value }
    func clear(_ key: String) { shown[key] = nil }
}

func verdict(model: String?, shown: String?) -> String {
    switch (model, shown) {
    case (nil, nil): return "olcum-disi"
    case (nil, _?): return "artik"
    case (_?, nil): return "eksik"
    case let (wanted?, displayed?): return wanted == displayed ? "uyumlu" : "bayat"
    }
}

func check(_ label: String, model: [String: String], screen: Screen, keys: [String]) -> [String] {
    keys.map { key in
        let result = verdict(model: model[key], shown: screen.shown[key])
        print("\(label) | \(screen.name).\(key): model=\(model[key] ?? "-") ekran=\(screen.shown[key] ?? "-") -> \(result)")
        return result
    }
}

func countVerdicts(_ verdicts: [String]) {
    for name in ["uyumlu", "bayat", "eksik", "artik"] {
        print("\(name): \(verdicts.filter { $0 == name }.count)")
    }
}
```

Kural nedeni görmez, yalnız iki değere bakar. Bir değerin ekranda eksik mi, bayat mı, artık mı kaldığını ayırmak için nedenini bilmek gerekmez; bu yüzden aynı kural iki çerçevenin ekranına da uygulanabilir. `Screen` ekranın gösterdiği değerleri tutar; ekrana yazan her çağrı `set` ile modelde de yazar.

#### Buyurgan Ekrana Gömülen Bildirimsel Görünüm

Bir görev ekranı düşünelim. Üstte bir özet şeridi var: başlık, açık görev sayısı, süzgeç. Şerit bildirimsel çerçevede yazıldı, ekranın geri kalanı buyurgan çerçevede duruyor. Bildirimsel görünüm buyurgan bir ekrana **barındırma denetleyicisi** ile girer: görünümü içine alan, onu çocuk denetleyici olarak ekleyen bir sınıftır. Kök görünüm denetleyicinin `rootView` özelliğidir.

```swift
import UIKit
import SwiftUI

final class HostingController: UIViewController {
    private var host: UIHostingController<SummaryView>!
    var summary = SummaryState(title: "Gorevler", openCount: 3, filter: "acik")

    override func viewDidLoad() {
        super.viewDidLoad()
        host = UIHostingController(rootView: SummaryView(state: summary))
        addChild(host)
        view.addSubview(host.view)
        host.didMove(toParent: self)
    }

    func summaryDidChange() {
        // host.rootView = SummaryView(state: summary)
    }
}
```

Dikkat edilecek yer `summaryDidChange`. Bildirimsel çerçevede bir görünümün girdisi değişince çerçeve görünümü yeniden hesaplar. Ama burada girdi, `SummaryView`'ün oluşturucusuna verilmiş bir **kopyadır**: `summary` özelliği sonradan değişse bile kök görünümün elindeki durum eskisi gibi kalır. Çerçevenin yeniden hesaplaması ancak `rootView` yeniden atandığında devreye girer. Atama satırı yoksa değişikliği çerçeve görmez; bu buyurgan tarafın işidir.

Aşağıdaki model, kodun ekrana ne yazdığını kurar; çerçevenin kendisi değildir. Barındırma denetleyicisi saf Swift ile yeniden kuruldu ve KAYNAK parçadaki adları taşıyor: `HostingController`, `rootView`, `SummaryView`, `SummaryState`. Ortak yardımcı dosya iki ekranı da kurar.

```swift
// Bridging.swift — iki ekranin saf Swift kurulumu, hangi atama ekrana ne yazar
struct SummaryState {
    var title: String
    var openCount: Int
    var filter: String

    var values: [String: String] {
        ["title": title, "openCount": String(openCount), "filter": filter]
    }
}

struct SummaryView {
    let state: SummaryState

    func draw(on screen: Screen) {
        screen.set("title", state.title)
        screen.set("openCount", String(state.openCount))
        screen.set("filter", state.filter)
    }
}

final class HostingController {
    let screen = Screen("HostingController")
    var rootView: SummaryView {
        didSet { rootView.draw(on: screen) }
    }

    init(rootView: SummaryView) {
        self.rootView = rootView
        rootView.draw(on: screen)
    }
}

final class LegacyListView {
    let screen = Screen("LegacyListView")

    init(title: String, rows: [String]) {
        screen.set("title", title)
        reloadData(rows: rows)
    }

    func reloadData(rows: [String]) {
        screen.set("rowCount", String(rows.count))
    }
}
```

İlk olay: ekran üç açık görevle kurulur, sonra model beşe çıkar ve `rootView` yeniden atanmaz.

```swift
// main.swift — model degisti, rootView yeniden atanmadi
let first = SummaryState(title: "Gorevler", openCount: 3, filter: "acik")
let latest = SummaryState(title: "Gorevler", openCount: 5, filter: "acik")

let hosting = HostingController(rootView: SummaryView(state: first))
let withoutAssignment = check("hosting-atamasiz", model: latest.values, screen: hosting.screen,
                              keys: ["title", "openCount", "filter"])

countVerdicts(withoutAssignment)
```

```
hosting-atamasiz | HostingController.title: model=Gorevler ekran=Gorevler -> uyumlu
hosting-atamasiz | HostingController.openCount: model=5 ekran=3 -> bayat
hosting-atamasiz | HostingController.filter: model=acik ekran=acik -> uyumlu
uyumlu: 2
bayat: 1
eksik: 0
artik: 0
```

Üç alandan ikisi uyumludur, çünkü modelde değişmediler. `openCount` modelde 5, ekranda 3: **bayat**. Ekranda hiçbir hata görünmez, şerit güzelce çizilmiştir; yalnız rakam eskidir. Derleyici de uyarmaz, çünkü ortada yanlış yazılmış bir satır yok, yazılmamış bir satır var. Kural nedeni söylemez, yalnız iki değerin ayrıştığını söyler. Bu tür hatalar gözle bulunduğunda genellikle ekranı ikinci kez açan biri tarafından fark edilir; ölçüm aynı kontrolü her alan için baştan sona yapar.

**KP1.** Model, `rootView` ataması yapıldığında bildirimsel görünümün yeni durumu ekrana yazdığını kabul eder. Bildirimsel çerçevenin atamadan sonra görünümü nasıl, hangi anda yeniden hesapladığı ölçülmez; `draw(on:)` o hesabın sonucunu tek çağrıda yazar. Bu bir varsayımdır ve yalnız bu sınırdan geçen değer için geçerlidir.

Şimdi satırı açalım ve aynı değişikliği bir de atamayla uygulayalım. Bu olayda yalnız bir şey değişiyor: `rootView` yeni durumla yeniden atanıyor. Başlangıç değerleri, model ve denetlenen alanlar öncekiyle aynıdır, böylece iki çıktı arasındaki fark tek bir satıra bağlanır.

```swift
// main.swift — model degisti, rootView yeniden atandi
let first = SummaryState(title: "Gorevler", openCount: 3, filter: "acik")
let latest = SummaryState(title: "Gorevler", openCount: 5, filter: "acik")

let hosting = HostingController(rootView: SummaryView(state: first))
hosting.rootView = SummaryView(state: latest)
let withAssignment = check("hosting-atamali", model: latest.values, screen: hosting.screen,
                           keys: ["title", "openCount", "filter"])

countVerdicts(withAssignment)
```

```
hosting-atamali | HostingController.title: model=Gorevler ekran=Gorevler -> uyumlu
hosting-atamali | HostingController.openCount: model=5 ekran=5 -> uyumlu
hosting-atamali | HostingController.filter: model=acik ekran=acik -> uyumlu
uyumlu: 3
bayat: 0
eksik: 0
artik: 0
```

Üç alan da uyumlu. Düzeltme tek satırdır, ama o satırı yazmayı hatırlamak programcıya kalır. Bildirimsel çerçevede bu işi çerçeve yapıyordu; barındırma denetleyicisi o işi yeniden buyurgan tarafa verir. Çözüm de aynı yerde yatar: değişimi gören kod, yani `summaryDidChange`, kök görünümü yeniden atamalıdır. Birden çok yerden değişebilen bir durum için bu çağrıyı tek bir noktaya toplamak, atamanın unutulacağı yer sayısını azaltır.

#### Bildirimsel Ekrana Sarılan Buyurgan Görünüm

Dikiş ters yönde de çalışır. Eski bir liste ekranı yıllardır sağlam çalışıyor ve yeni bildirimsel ekran onu olduğu gibi içine almak istiyor. Buyurgan görünümü bildirimsel ağaca sokan şey **sarmalama**dır: iki zorunlu yöntemli bir tip, biri görünümü bir kez kurar, öteki bildirimsel durum değiştikçe buyurgan görünümü günceller.

```swift
import SwiftUI

struct ListRepresentable: UIViewRepresentable {
    let title: String
    let rows: [String]

    func makeUIView(context: Context) -> LegacyListView {
        LegacyListView(title: title, rows: rows)
    }

    func updateUIView(_ list: LegacyListView, context: Context) {
        // list.reloadData(rows: rows)
    }
}
```

`makeUIView` ekran açılırken bir kez çalışır. `updateUIView(_:context:)` ise bildirimsel durum her değiştiğinde çağrılır; buyurgan görünüme yeni değeri taşımak onun işidir. Burada gövdesi boş bırakılmış: model, çerçeve çağrıyı yaptığında hiçbir şey yazılmadığı durumu kurar.

**KP2.** Model, çerçevenin durum değişince `updateUIView` yöntemini çağırdığını kabul eder; ölçümde çağrıyı kodumuz yapar. Çağrının zamanı ve sıklığı ölçülmez. Ölçülen şey, çağrı yapıldığında gövdenin buyurgan ekrana ne yazdığıdır.

```swift
// main.swift — updateUIView gövdesi bos, liste yeniden yuklenmez
struct ListRepresentable {
    let title: String
    let rows: [String]

    func makeUIView() -> LegacyListView {
        LegacyListView(title: title, rows: rows)
    }

    func updateUIView(_ list: LegacyListView) {
        // bos
    }
}

let before = ListRepresentable(title: "Gorevler", rows: ["a", "b", "c"])
let after = ListRepresentable(title: "Gorevler", rows: ["a", "b", "c", "d", "e"])

let list = before.makeUIView()
after.updateUIView(list)

let model = ["title": "Gorevler", "rowCount": String(after.rows.count)]
let emptyUpdate = check("sarmalama-bos", model: model, screen: list.screen,
                        keys: ["title", "rowCount"])

countVerdicts(emptyUpdate)
```

```
sarmalama-bos | LegacyListView.title: model=Gorevler ekran=Gorevler -> uyumlu
sarmalama-bos | LegacyListView.rowCount: model=5 ekran=3 -> bayat
uyumlu: 1
bayat: 1
eksik: 0
artik: 0
```

Başlık ilk kurulumda yazıldı ve değişmedi: uyumlu. Satır sayısı modelde 5, liste ise ilk üç satırla kalmış: bayat. Bu ilk olayla aynı biçimdir; yalnız sınırı ters yönden geçiyoruz. İki durumda da değeri taşıyacak çağrı yazılmamıştır. Gömmede eksik olan atamaydı, sarmalamada eksik olan `reloadData` çağrısıdır; kural ikisini aynı sözcükle, `bayat` ile adlandırır.

Şimdi gövdeyi yazalım. Parça, bildirimsel durumdaki `rows` değerini buyurgan görünüme `reloadData` ile aktarır; geri kalan her şey önceki parçayla aynıdır.

```swift
import SwiftUI

struct ListRepresentable: UIViewRepresentable {
    let title: String
    let rows: [String]

    func makeUIView(context: Context) -> LegacyListView {
        LegacyListView(title: title, rows: rows)
    }

    func updateUIView(_ list: LegacyListView, context: Context) {
        list.reloadData(rows: rows)
    }
}
```

Aynı iki durumu, bu sefer `reloadData` çağrısıyla ölçelim.

```swift
// main.swift — updateUIView gövdesi listeyi yeniden yukler
struct ListRepresentable {
    let title: String
    let rows: [String]

    func makeUIView() -> LegacyListView {
        LegacyListView(title: title, rows: rows)
    }

    func updateUIView(_ list: LegacyListView) {
        list.reloadData(rows: rows)
    }
}

let before = ListRepresentable(title: "Gorevler", rows: ["a", "b", "c"])
let after = ListRepresentable(title: "Gorevler", rows: ["a", "b", "c", "d", "e"])

let list = before.makeUIView()
after.updateUIView(list)

let model = ["title": "Gorevler", "rowCount": String(after.rows.count)]
let forwardedUpdate = check("sarmalama-dolu", model: model, screen: list.screen,
                            keys: ["title", "rowCount"])

countVerdicts(forwardedUpdate)
```

```
sarmalama-dolu | LegacyListView.title: model=Gorevler ekran=Gorevler -> uyumlu
sarmalama-dolu | LegacyListView.rowCount: model=5 ekran=5 -> uyumlu
uyumlu: 2
bayat: 0
eksik: 0
artik: 0
```

İki alan da uyumlu. Sarmalamanın işi budur: bildirimsel durumun, görünüm kurulduktan sonra buyurgan görünüme ulaşma yolu `updateUIView`'dir ve yazılan satır kadar taşır. Eski liste ekranının kendi yaşam döngüsü bu çağrıdan habersizdir; bildirimsel çerçeve değişikliği duyurur, ama duyuruyu buyurgan görünüme çeviren kod sizsiniz.

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

Bu bölümde ölçümün kapsamını ayırıyoruz. Şimdiye kadar iki ekran da yalnız kodun yazdığı alanları gösterdi. Bildirimsel görünümün **kendi** içinde sakladığı bir durum ise farklıdır: şeridin açılıp kapanan bir bölümü, bir kaydırma konumu, bir odak. Bu değerleri çerçeve kendi başına yeniden hesaplar ve ekran kodu onları `Screen`'e yazmaz.

```swift
// main.swift — bildirimsel tarafin kendi durumu ekrana hic yazilmaz
let state = SummaryState(title: "Gorevler", openCount: 5, filter: "acik")
let hosting = HostingController(rootView: SummaryView(state: state))

let model = ["openCount": "5"]
let ownState = check("kendi-durum", model: model, screen: hosting.screen,
                     keys: ["openCount", "isExpanded"])

countVerdicts(ownState)
```

```
kendi-durum | HostingController.openCount: model=5 ekran=5 -> uyumlu
kendi-durum | HostingController.isExpanded: model=- ekran=- -> olcum-disi
uyumlu: 1
bayat: 0
eksik: 0
artik: 0
```

`isExpanded` iki tarafta da yok: sonuç `olcum-disi` ve sayıma girmez. Bu alanın ekranda doğru olup olmadığını kural söylemez, çünkü model onu hiçbir zaman kurmadı. Uyumlu sayısının tam çıkması, ekranın her şeyinin güncel olduğu anlamına gelmez; yalnız yazılan alanların güncel olduğunu söyler. Bildirimsel tarafın kendi kendine yeniden hesaplaması bu ölçümün dışında kalır. Yukarıdaki iki varsayım da bu yüzden açıkça yazıldı: model, bildirimsel tarafın yaptığı işi atama ve çağrı noktasında bir kez yazılmış sayar. Gerçek çerçevenin bu işi ne zaman yaptığını anlatmak ders metninin ve çerçeve belgelerinin işidir, ölçümün değil. Bu ders yalnız bir şeyi iddia eder: değer sınırdan geçirilecek çağrıyı kodun kendisi yazmadıysa, ekran modelden geri kalır.

#### Özet

- Bildirimsel görünüm buyurgan bir ekrana barındırma denetleyicisiyle girer; `rootView` yeniden atanmazsa model beşe çıkarken ekran üçte kalır ve açık görev sayısı **bayat** ölçülür (2 uyumlu, 1 bayat).
- Atama yazıldığında aynı üç alanın üçü de uyumludur (3 uyumlu).
- Buyurgan görünüm bildirimsel ekrana sarmalamayla girer; `updateUIView` gövdesi boşsa satır sayısı modelde 5, ekranda 3 kalır (1 uyumlu, 1 bayat). Gövdeye `reloadData` yazılınca iki alan da uyumludur (2 uyumlu).
- Her iki yönde de değeri sınırdan geçiren çağrıyı buyurgan taraf yazar; çağrı yoksa kural yalnız ayrışmayı görür, nedenini söylemez.
- Bildirimsel tarafın kendi durumu modelde kurulmadığında sonuç `olcum-disi` olur ve sayıma girmez (1 uyumlu, 0 diğer).
- Bu dersin uyumu: **bayat**.

#### Sonraki Adım

Bir sonraki derste gömme ve sarmalama tek ekranda kalmaz, bir uygulamanın tamamına yayılır: eski kodun ekran ekran bildirimsel çerçeveye dönüşümünü ele alacağız. Orada sorunun yönü değişecek, değer sınırdan geçerken değil, yeni ekranda yerini bulamadığında ne olduğuna bakacağız.

### Eski Kodun Kademeli Dönüşümü

Source: https://academia.sh/tr/kurslar/uikit/eski-kodun-kademeli-donusumu

Eski bir ekranı ekran ekran yeni çerçeveye taşırken taşınmayan alanın yeni ekranda eksik kaldığının, modelden kalkan alanın eski ekranda artık kaldığının ve kuralın ekran düzeyinde değil alan başına çalıştığının ölçümü.

Önceki derste (Bildirimsel ve Buyurgan Arayüz Bir Arada) iki çerçevenin dikiş yerine baktık: değer sınırdan geçerken atama ya da güncelleme çağrısı yazılmazsa alan bayat kalıyordu. Gerçek bir uygulamada o dikişin yanında ikinci bir soru vardır. Eski kod yüzlerce ekran içerir ve hiçbir takım hepsini bir gecede yeniden yazmaz. Dönüşüm ekran ekran ilerler: her adımda bir ekran bildirimsel çerçeveye taşınır, geri kalanı eski hâlinde çalışmaya devam eder. Uygulama dönüşümün ortasında da yayımlanabilir olmak zorundadır.

Bu, iki çerçevenin uzun süre yan yana yaşaması demektir. Kademeli yolun gerekçesi basittir: her adım küçük olduğu için hata bulunduğunda kaynağı bellidir. Bir ekran taşındığında bozulabilecek şey yalnız o ekrandır. Tek seferlik yeniden yazımda aynı hata uygulamanın herhangi bir yerinden çıkabilir ve ne zaman girdiği bilinmez. Taşıma sırası genellikle yaprak ekranlardan köke doğrudur: başka ekrana açılmayan, az alanlı bir ayrıntı ekranı ilk aday olur, çünkü hem küçüktür hem de hata verirse geri alınması kolaydır. Dikişte ise önceki dersin ikisi de geçerlidir: yeni ekran eski bir gezinme yığıtına barındırma denetleyicisiyle itilir ya da eski bir parça yeni ekrana sarmalamayla girer.

Bu derste ölçeceğimiz şey, bir ekran taşınırken eski ekranın yaptığı işlerden hangisinin yeni ekrana geçtiğidir. Ölçü ekseni değişmedi: **uyum**. Alan başına dört sonuçtan biri çıkar: `uyumlu`, `bayat`, `eksik`, `artik`. Çekirdek dosya önceki derslerle aynıdır.

```swift
// Sync.swift — ekranda gosterilen her alani modelin o anki degeriyle karsilastirir
final class Screen {
    let name: String
    private(set) var shown: [String: String] = [:]

    init(_ name: String) { self.name = name }

    func set(_ key: String, _ value: String) { shown[key] = value }
    func clear(_ key: String) { shown[key] = nil }
}

func verdict(model: String?, shown: String?) -> String {
    switch (model, shown) {
    case (nil, nil): return "olcum-disi"
    case (nil, _?): return "artik"
    case (_?, nil): return "eksik"
    case let (wanted?, displayed?): return wanted == displayed ? "uyumlu" : "bayat"
    }
}

func check(_ label: String, model: [String: String], screen: Screen, keys: [String]) -> [String] {
    keys.map { key in
        let result = verdict(model: model[key], shown: screen.shown[key])
        print("\(label) | \(screen.name).\(key): model=\(model[key] ?? "-") ekran=\(screen.shown[key] ?? "-") -> \(result)")
        return result
    }
}

func countVerdicts(_ verdicts: [String]) {
    for name in ["uyumlu", "bayat", "eksik", "artik"] {
        print("\(name): \(verdicts.filter { $0 == name }.count)")
    }
}
```

Eski ekranın yaptığı işler çoğu zaman yazılı bir yerde durmaz. Eski ekranı yazan kişi ayrılmıştır, belge yoktur, ekran yıllar içinde küçük eklerle büyümüştür. Yeni ekranı yazan kişi eski ekranı okuyup yeniden kurar ve okurken bir alanı gözden kaçırabilir. Kural bu gözden kaçmayı kimin yaptığına bakmadan, alanın modelde olup ekranda olmadığını söyleyerek yakalar. Bu yüzden dönüşüm adımının bitiş ölçütü "ekran açılıyor" değildir; "modelin her alanı ekranda karşılık buluyor" olmalıdır. İlk ölçüt gözle, ikincisi alan alan karşılaştırarak sağlanır.

#### Taşınmayan Alan

Elimizde bir görev ekranı var. Eski sürüm buyurgan arayüz çerçevesinde yazılmış ve üç değer gösteriyor: başlık, açık görev sayısı ve teslim etiketi. Her değer bir **bağlantı (outlet)** üzerinden etikete yazılır; yazan kod ekran görünmeden hemen önce çalışan bir yöntemdir. Ayrıca ekranda bir de eski bir rozet var.

```swift
import UIKit

final class LegacyTaskController: UIViewController {
    @IBOutlet private var titleLabel: UILabel!
    @IBOutlet private var countLabel: UILabel!
    @IBOutlet private var dueLabel: UILabel!
    @IBOutlet private var badgeLabel: UILabel!

    func bind(_ task: TaskModel) {
        titleLabel.text = task.title
        countLabel.text = String(task.openCount)
        dueLabel.text = task.dueLabel
        badgeLabel.text = "yeni"
    }
}
```

Dönüşümün ilk adımında ekran bildirimsel çerçevede yeniden yazılır. Yeni görünüm başlığı ve sayıyı alır ve gösterir. Teslim etiketi yeniden yazım sırasında atlanmıştır.

```swift
import SwiftUI

struct TaskScreenView: View {
    let task: TaskModel

    var body: some View {
        VStack {
            Text(task.title)
            Text(String(task.openCount))
        }
    }
}
```

İki parçayı karşılaştırınca fark görülebilir, ama çoğu gerçek ekranda alan sayısı on ikidir ve fark gözden kaçar. Model iki ekranı da saf Swift ile kurar: kim neyi ekrana yazıyor. Ortak tipler yardımcı dosyada durur; KAYNAK parçalardaki `TaskModel`, `LegacyTaskController`, `bind` ve `body` adları burada da aynıdır. Etiket adlarının sonundaki `Label` atılırsa anahtarlar çıkar: `titleLabel` için `title`, `dueLabel` için `dueLabel`.

```swift
// Migration.swift — modelin degeri ve eski ekranin kurulumu
struct TaskModel {
    var title: String
    var openCount: Int
    var dueLabel: String

    var values: [String: String] {
        ["title": title, "openCount": String(openCount), "dueLabel": dueLabel]
    }
}

final class LegacyTaskController {
    let screen = Screen("LegacyTaskController")

    func bind(_ task: TaskModel) {
        screen.set("title", task.title)
        screen.set("openCount", String(task.openCount))
        screen.set("dueLabel", task.dueLabel)
        screen.set("badge", "yeni")
    }
}
```

Model, kodun ekrana ne yazdığını kurar; çerçevenin kendisi değildir. Birinci olay: yeni ekran, `dueLabel` yazılmadan kurulur ve model üç alanı da ister.

```swift
// main.swift — yeni ekran dueLabel alanini tasimaz
struct TaskScreenView {
    let task: TaskModel

    func body(on screen: Screen) {
        screen.set("title", task.title)
        screen.set("openCount", String(task.openCount))
    }
}

let task = TaskModel(title: "Gorevler", openCount: 5, dueLabel: "yarin")
let migrated = Screen("TaskScreenView")
TaskScreenView(task: task).body(on: migrated)

let dropped = check("yeni-ekran-eksik", model: task.values, screen: migrated,
                    keys: ["title", "openCount", "dueLabel"])

countVerdicts(dropped)
```

```
yeni-ekran-eksik | TaskScreenView.title: model=Gorevler ekran=Gorevler -> uyumlu
yeni-ekran-eksik | TaskScreenView.openCount: model=5 ekran=5 -> uyumlu
yeni-ekran-eksik | TaskScreenView.dueLabel: model=yarin ekran=- -> eksik
uyumlu: 2
bayat: 0
eksik: 1
artik: 0
```

İki alan taşındı ve uyumlu. `dueLabel` modelde `yarin`, yeni ekranda hiç yok: **eksik**. Bu `bayat`tan farklı bir durumdur. Bayatta ekranda eski bir değer vardı; burada ekran o alanı hiç yazmamıştır. Bir önceki dersteki hatayla kök nedenleri aynı kökten, yazılmamış bir satırdan gelir, ama sonuç ayrı adlandırılır, çünkü ayrı biçimde fark edilir. Bayat bir rakam yanlış okunabilir, eksik bir etiket ise ekranda boşluk olarak görünür.

**KP1.** Model, yeni ekranın `body` içinde yazdığı her değeri ekrana yazılmış kabul eder. Bildirimsel çerçevenin `body`yi ne zaman çağırdığı ve değişiklikte neyi yeniden hesapladığı ölçülmez; ölçülen şey, `body`nin hangi alanları verdiğidir.

Eksik alanın düzeltilmesi kısadır: yeni görünüme `Text(task.dueLabel)` eklemek. Ölçümde bunu `body` içine bir satır yazarak yapıyoruz.

```swift
// main.swift — dueLabel yeni ekrana tasindi
struct TaskScreenView {
    let task: TaskModel

    func body(on screen: Screen) {
        screen.set("title", task.title)
        screen.set("openCount", String(task.openCount))
        screen.set("dueLabel", task.dueLabel)
    }
}

let task = TaskModel(title: "Gorevler", openCount: 5, dueLabel: "yarin")
let migrated = Screen("TaskScreenView")
TaskScreenView(task: task).body(on: migrated)

let carried = check("yeni-ekran-tasindi", model: task.values, screen: migrated,
                    keys: ["title", "openCount", "dueLabel"])

countVerdicts(carried)
```

```
yeni-ekran-tasindi | TaskScreenView.title: model=Gorevler ekran=Gorevler -> uyumlu
yeni-ekran-tasindi | TaskScreenView.openCount: model=5 ekran=5 -> uyumlu
yeni-ekran-tasindi | TaskScreenView.dueLabel: model=yarin ekran=yarin -> uyumlu
uyumlu: 3
bayat: 0
eksik: 0
artik: 0
```

Üç alanın üçü de uyumlu. Dönüşüm bu alan için tamamlandı. Dikkat edilecek bir nokta daha var: bu düzeltme `body`ye bir satır ekledi, ama başka bir şey değiştirmedi. Kademeli dönüşümün işe yarar tarafı budur. Hatanın yeri bellidir, düzeltmesi küçüktür ve düzeltmeden sonra ölçüm hatanın gittiğini aynı sayılarla gösterir.

#### Modelden Kalkan Alan

Dönüşümün ters yüzü, eski ekranın hâlâ yazdığı ama artık hiçbir şeyin karşılığı olmayan alandır. Rozeti hatırlayalım: eski ekranda `badgeLabel.text = "yeni"` satırı var ve bu satır koşulsuz çalışıyor. Diyelim ki bir ürün kararıyla rozet kaldırıldı. Model artık `badge` alanı taşımaz, ama eski ekran onu yazmaya devam eder.

```swift
// main.swift — model badge alanini tasimaz, eski ekran yazmaya devam eder
let task = TaskModel(title: "Gorevler", openCount: 5, dueLabel: "yarin")
let legacy = LegacyTaskController()
legacy.bind(task)

let leftover = check("eski-ekran-artik", model: task.values, screen: legacy.screen,
                     keys: ["title", "badge"])

countVerdicts(leftover)
```

```
eski-ekran-artik | LegacyTaskController.title: model=Gorevler ekran=Gorevler -> uyumlu
eski-ekran-artik | LegacyTaskController.badge: model=- ekran=yeni -> artik
uyumlu: 1
bayat: 0
eksik: 0
artik: 1
```

`title` uyumlu. `badge` için model değeri yok (`-`), ekranda `yeni` yazıyor: **artik**. Kural yine nedeni görmez; alanın kaldırıldığını mı, yoksa eski ekranın yanlışlıkla yazdığını mı ayıramaz. Yalnız ekranda modelin bilmediği bir değer olduğunu söyler. Eski kodda bu türden satırlar çoktur: kaldırılmış bir özelliğin kalıntısı, bir hata ayıklama için konmuş sabit bir metin, artık kimsenin okumadığı bir etiket ve yeni ekrana taşınırken en kolay yok olanlar da bunlardır; ama bir alan taşınmadığında da, eksik ile artık aynı soruya iki ayrı cevap verir: modelde olan ekranda yok, ekranda olan modelde yok.

Artığın iki çözümü vardır ve hangisinin doğru olduğunu ölçüm söylemez. Rozet gerçekten kaldırıldıysa eski ekrandaki satır silinir ve alan sonuçtan düşer. Rozet kaldırılmadıysa, model ona yeniden bir değer verir ve sonuç uyumlu olur. İlk çözüm eski ekranı değiştirir, ikincisi modeli. Aynı ayrım eksik için de geçerlidir: ya yeni ekrana alan eklenir ya da model o alanı bırakır. Dönüşümde kimin hangisini seçeceği ürünle ilgili bir karardır; kural yalnız iki tarafın ayrıştığını gösterir ve karar verilene kadar ayrışmanın görünür kalmasını sağlar.

Dönüşüm planına bu yüzden bir envanter adımı konur. Taşınacak ekranın modelden okuduğu her alan listelenir, eski ekranın kendi başına yazdığı sabitler ayrıca işaretlenir. Envanter çıkmadan taşınan bir ekran, ölçümde ilk iki sonucu, eksik ve artığı, birlikte verir. Envanter çıktıktan sonra ise taşınan her ekran için kontrol tek bir soruya iner: bu ekranın alanları, eski ekranın alanlarıyla ve modelin alanlarıyla aynı mı?

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

Dönüşümün ortasında aynı model iki ekranı birden besler: eski ekran henüz yerinde, yeni ekran yanında. Soru, "bu ekran modelle uyumlu mu" sorusunun ekran düzeyinde cevaplanıp cevaplanamayacağıdır.

```swift
// main.swift — ayni model iki ekrana karsi denetlenir
struct TaskScreenView {
    let task: TaskModel

    func body(on screen: Screen) {
        screen.set("title", task.title)
        screen.set("openCount", String(task.openCount))
    }
}

let task = TaskModel(title: "Gorevler", openCount: 5, dueLabel: "yarin")
let keys = ["title", "openCount", "dueLabel"]

let legacy = LegacyTaskController()
legacy.bind(task)
let migrated = Screen("TaskScreenView")
TaskScreenView(task: task).body(on: migrated)

let oldScreen = check("iki-ekran-eski", model: task.values, screen: legacy.screen, keys: keys)
let newScreen = check("iki-ekran-yeni", model: task.values, screen: migrated, keys: keys)

countVerdicts(oldScreen + newScreen)
```

```
iki-ekran-eski | LegacyTaskController.title: model=Gorevler ekran=Gorevler -> uyumlu
iki-ekran-eski | LegacyTaskController.openCount: model=5 ekran=5 -> uyumlu
iki-ekran-eski | LegacyTaskController.dueLabel: model=yarin ekran=yarin -> uyumlu
iki-ekran-yeni | TaskScreenView.title: model=Gorevler ekran=Gorevler -> uyumlu
iki-ekran-yeni | TaskScreenView.openCount: model=5 ekran=5 -> uyumlu
iki-ekran-yeni | TaskScreenView.dueLabel: model=yarin ekran=- -> eksik
uyumlu: 5
bayat: 0
eksik: 1
artik: 0
```

Eski ekran üç alanda da uyumlu, yeni ekran iki alanda uyumlu, birinde eksik. Toplamda beş uyumlu, bir eksik. Bu toplamdan "uygulamanın yüzde seksen üçü uyumlu" gibi bir cümle kurmak mümkün olurdu, ama bu cümle ekranın hangisinin tutarlı olduğunu söylemez. Kullanıcı yalnız yeni ekranı görüyorsa onun gözünde ekran bozuktur; yalnız eskisini görüyorsa sorun yoktur. Ekran düzeyinde tek bir yanıt yoktur, **kural alan başınadır**. Aynı sebeple bir ekranı "uyumlu" ilan etmek, yazılan her alanın modelle eşit olduğunu söyler; kuralın bilmediği alan modelde olsa da ekranda eksik kalırsa yalnız sayıda görünür.

Bu ölçüm modelin çerçeveden ayrıldığı yeri de adlandırır. **KP2.** Model, eski ekranın `bind` içinde yazdığı her değeri ekrana yazılmış kabul eder; etiket metninin yerleşimi, yazı tipi ya da kırpılması ölçülmez. Ölçülen şey, `bind` çağrısının hangi anahtarlara değer yazdığıdır.

Gerçek dönüşümde iki ekran aynı kullanıcıya aynı anda gösterilmez; biri yığıttan çıkar, öteki girer. Modelde ikisi birden vardır, çünkü kural bir ekranı değil bir alanı sorgular ve alan başına sonuç veren bir kural için ikisini yan yana koymak yeterlidir.

Ölçümün sınırı şudur: kural iki ekranı birbiriyle değil, her birini ayrı ayrı modelle karşılaştırır. Eski ekran ile yeni ekran arasında doğrudan bir karşılaştırma yoktur; ikisi de modele göre ölçülür. Modelin kendisi yanlışsa, örneğin bir alan modelde hiç tanımlanmamışsa, iki ekran da aynı eksiği taşıyabilir ve kural ikisini de uyumlu bulur. Kuralın kapsamı yalnız modelin bilinen alanlarıdır.

Dönüşüm bittiğinde son bir adım kalır: eski ekranın silinmesi. Silinen ekranla birlikte onun yazdığı artık alanlar da gider. Yeni ekranın hâlâ eksik bıraktığı bir alan ise bu noktadan sonra karşılaştırılacak bir eski ekran bulamaz ve eksiklik ancak model üzerinden fark edilir. Kademeli dönüşümün sağladığı güvence, eski ekran yerindeyken her alanın iki ekranda da aynı sonucu vermesidir; bu güvence eski ekran silindikten sonra ölçüm çalıştırılmadıkça kaybolur.

#### Özet

- Eski kod ekran ekran dönüşür; dönüşümün ortasında iki çerçeve aynı uygulamada yaşar ve aynı modeli besler.
- Yeni ekrana taşınmayan `dueLabel` alanı modelde `yarin` iken ekranda hiç yoktur: eksik ölçülür (2 uyumlu, 1 eksik).
- Alan taşındığında üç alan da uyumludur (3 uyumlu).
- Modelden kalkan `badge` alanını eski ekran yazmaya devam ettiğinde sonuç artıktır (1 uyumlu, 1 artık).
- Aynı model iki ekrana karşı denetlendiğinde eski ekran üç alanda, yeni ekran iki alanda uyumludur; toplam 5 uyumlu, 1 eksik. Ekran düzeyinde tek yanıt yoktur, kural alan başınadır.
- Bu dersin uyumu: **eksik**.

#### Sonraki Adım

Bir sonraki ders konunun ve kursun kapanışıdır: Çerçeve Seçimi. İki çerçeveyi aynı kuralla ölçmüş olduk; hangisini ne zaman seçeceğiz sorusuna oradan bakacağız.

### Çerçeve Seçimi

Source: https://academia.sh/tr/kurslar/uikit/cerceve-secimi

Aynı modelden bildirimsel ve buyurgan iki ekran kurup aynı kuralla denetlemenin ölçümü ve uyumun bir çerçeve seçim ölçütü olmadığının gösterimi; kursun on beş dersinin kapanış tablosu.

Önceki derste eski bir ekranı ekran ekran yeni çerçeveye taşıdık ve kuralın alan başına çalıştığını gördük. Bu konunun son sorusu kaçınılmaz olarak şudur: yeni bir ekran yazılacaksa hangi çerçeve seçilmeli? Kursun ölçüsü bu soruya cevap vermez, ve bu dersin işi tam olarak bunu göstermektir. Ölçü ekseni değişmedi: **uyum**. Çekirdek dosya önceki derslerle aynıdır.

```swift
// Sync.swift — ekranda gosterilen her alani modelin o anki degeriyle karsilastirir
final class Screen {
    let name: String
    private(set) var shown: [String: String] = [:]

    init(_ name: String) { self.name = name }

    func set(_ key: String, _ value: String) { shown[key] = value }
    func clear(_ key: String) { shown[key] = nil }
}

func verdict(model: String?, shown: String?) -> String {
    switch (model, shown) {
    case (nil, nil): return "olcum-disi"
    case (nil, _?): return "artik"
    case (_?, nil): return "eksik"
    case let (wanted?, displayed?): return wanted == displayed ? "uyumlu" : "bayat"
    }
}

func check(_ label: String, model: [String: String], screen: Screen, keys: [String]) -> [String] {
    keys.map { key in
        let result = verdict(model: model[key], shown: screen.shown[key])
        print("\(label) | \(screen.name).\(key): model=\(model[key] ?? "-") ekran=\(screen.shown[key] ?? "-") -> \(result)")
        return result
    }
}

func countVerdicts(_ verdicts: [String]) {
    for name in ["uyumlu", "bayat", "eksik", "artik"] {
        print("\(name): \(verdicts.filter { $0 == name }.count)")
    }
}
```

#### Aynı Model, İki Ekran

Bir profil ekranı üç değer gösteriyor: ad, okunmamış sayısı ve durum. Model tek bir değer tipidir. Bildirimsel çerçevede ekran bu modelin bir işlevidir.

```swift
import SwiftUI

struct ProfileView: View {
    let model: ProfileModel

    var body: some View {
        VStack {
            Text(model.title)
            Text(String(model.unread))
            Text(model.status)
        }
    }
}
```

Buyurgan çerçevede aynı üç değeri kod, üç etikete tek tek yazar.

```swift
import UIKit

final class ProfileController: UIViewController {
    @IBOutlet private var titleLabel: UILabel!
    @IBOutlet private var unreadLabel: UILabel!
    @IBOutlet private var statusLabel: UILabel!

    func render(_ model: ProfileModel) {
        titleLabel.text = model.title
        unreadLabel.text = String(model.unread)
        statusLabel.text = model.status
    }
}
```

Model, kodun ekrana ne yazdığını kurar; çerçevenin kendisi değildir. İki ekranın yazdığı değerler yardımcı dosyada durur ve adlar KAYNAK parçalardakilerle aynıdır.

```swift
// Profile.swift — ayni modelden kurulan iki ekran
struct ProfileModel {
    var title: String
    var unread: Int
    var status: String

    var values: [String: String] {
        ["title": title, "unread": String(unread), "status": status]
    }
}

struct ProfileView {
    let model: ProfileModel

    func body(on screen: Screen) {
        screen.set("title", model.title)
        screen.set("unread", String(model.unread))
        screen.set("status", model.status)
    }
}

final class ProfileController {
    let screen = Screen("ProfileController")

    func render(_ model: ProfileModel) {
        screen.set("title", model.title)
        screen.set("unread", String(model.unread))
        screen.set("status", model.status)
    }
}
```

İlk olay: aynı model iki ekrana verilir.

```swift
// main.swift — ayni model iki ekrana yazilir
let model = ProfileModel(title: "Ayse", unread: 3, status: "cevrimici")
let keys = ["title", "unread", "status"]

let declarative = Screen("ProfileView")
ProfileView(model: model).body(on: declarative)
let controller = ProfileController()
controller.render(model)

let viaDeclarative = check("ayni-model-bildirimsel", model: model.values, screen: declarative, keys: keys)
let viaImperative = check("ayni-model-buyurgan", model: model.values, screen: controller.screen, keys: keys)

countVerdicts(viaDeclarative + viaImperative)
```

```
ayni-model-bildirimsel | ProfileView.title: model=Ayse ekran=Ayse -> uyumlu
ayni-model-bildirimsel | ProfileView.unread: model=3 ekran=3 -> uyumlu
ayni-model-bildirimsel | ProfileView.status: model=cevrimici ekran=cevrimici -> uyumlu
ayni-model-buyurgan | ProfileController.title: model=Ayse ekran=Ayse -> uyumlu
ayni-model-buyurgan | ProfileController.unread: model=3 ekran=3 -> uyumlu
ayni-model-buyurgan | ProfileController.status: model=cevrimici ekran=cevrimici -> uyumlu
uyumlu: 6
bayat: 0
eksik: 0
artik: 0
```

Altı alanın altısı uyumlu. Buyurgan tarafta bunun sebebi kodun üç alanı da yazmasıdır; bildirimsel tarafta ise çerçevenin okuyanı yeniden hesaplamasıdır.

**KP21.** Model, bildirimsel görünümün `body` içinde yazdığı değerleri ekrana yazılmış kabul eder ve modelde yeniden hesaplamayı `body`yi yeniden çağırarak kurar. Çerçevenin hesabı ne zaman yaptığı ölçülmez; bu bir varsayımdır.

#### Buyurgan Tarafta Unutulan Alan

Aynı ekran buyurgan çerçevede yazılırken bir satırın düşmesi çok kolaydır. İki olay kuralım. Birincisinde denetleyici durum etiketini hiç yazmaz. İkincisinde ekran doğru kurulur, sonra model değişir ve değişimi gören kod ekranı yeniden yazmaz.

```swift
// main.swift — buyurgan denetleyici durumu unutur, sonra model degisir
final class ForgetfulProfileController {
    let screen = Screen("ForgetfulProfileController")

    func render(_ model: ProfileModel) {
        screen.set("title", model.title)
        screen.set("unread", String(model.unread))
    }
}

let model = ProfileModel(title: "Ayse", unread: 3, status: "cevrimici")
let keys = ["title", "unread", "status"]

let forgetful = ForgetfulProfileController()
forgetful.render(model)
let declarative = Screen("ProfileView")
ProfileView(model: model).body(on: declarative)

let missingDeclarative = check("buyurgan-unuttu-bildirimsel", model: model.values, screen: declarative, keys: keys)
let missingImperative = check("buyurgan-unuttu-buyurgan", model: model.values, screen: forgetful.screen, keys: keys)

let latest = ProfileModel(title: "Ayse", unread: 5, status: "cevrimici")
let refreshed = Screen("ProfileView")
ProfileView(model: latest).body(on: refreshed)
let careful = ProfileController()
careful.render(model)

let changedDeclarative = check("model-degisti-bildirimsel", model: latest.values, screen: refreshed, keys: keys)
let changedImperative = check("model-degisti-buyurgan", model: latest.values, screen: careful.screen, keys: keys)

countVerdicts(missingDeclarative + missingImperative + changedDeclarative + changedImperative)
```

```
buyurgan-unuttu-bildirimsel | ProfileView.title: model=Ayse ekran=Ayse -> uyumlu
buyurgan-unuttu-bildirimsel | ProfileView.unread: model=3 ekran=3 -> uyumlu
buyurgan-unuttu-bildirimsel | ProfileView.status: model=cevrimici ekran=cevrimici -> uyumlu
buyurgan-unuttu-buyurgan | ForgetfulProfileController.title: model=Ayse ekran=Ayse -> uyumlu
buyurgan-unuttu-buyurgan | ForgetfulProfileController.unread: model=3 ekran=3 -> uyumlu
buyurgan-unuttu-buyurgan | ForgetfulProfileController.status: model=cevrimici ekran=- -> eksik
model-degisti-bildirimsel | ProfileView.title: model=Ayse ekran=Ayse -> uyumlu
model-degisti-bildirimsel | ProfileView.unread: model=5 ekran=5 -> uyumlu
model-degisti-bildirimsel | ProfileView.status: model=cevrimici ekran=cevrimici -> uyumlu
model-degisti-buyurgan | ProfileController.title: model=Ayse ekran=Ayse -> uyumlu
model-degisti-buyurgan | ProfileController.unread: model=5 ekran=3 -> bayat
model-degisti-buyurgan | ProfileController.status: model=cevrimici ekran=cevrimici -> uyumlu
uyumlu: 10
bayat: 1
eksik: 1
artik: 0
```

Unutulan `status` alanı buyurgan ekranda **eksik**, güncellenmeyen `unread` alanı **bayat** çıktı; bildirimsel ekranın altı alanı uyumludur. Yani buyurgan çerçeve, programcının yazmayı unutabileceği yer sayısını artırır. Bu bir kusur değil, çerçevenin işi programcıya geri vermesinin bedelidir: ekran üzerindeki denetim de o kadar fazladır.

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

Bu bedel, uyumu bir seçim ölçütü yapmaz. Kural ekranı hangi çerçevenin kurduğunu bilmez; ekranın ne gösterdiğine bakar ve bozulma iki yönde de mümkündür. Titiz yazılmış bir buyurgan denetleyici tüm alanları uyumlu tutar. Bildirimsel bir görünüm ise modelde olmayan sabit bir metni gösterebilir; sabit metin her yeniden hesaplamada aynen yazılır.

```swift
// main.swift — bozulma iki cercevede de olur
struct BannerProfileView {
    let model: ProfileModel

    func body(on screen: Screen) {
        screen.set("title", model.title)
        screen.set("unread", String(model.unread))
        screen.set("status", model.status)
        screen.set("banner", "yeni")
    }
}

let model = ProfileModel(title: "Ayse", unread: 3, status: "cevrimici")
let keys = ["title", "unread", "status", "banner"]

let careful = ProfileController()
careful.render(model)
let banner = Screen("BannerProfileView")
BannerProfileView(model: model).body(on: banner)

let carefulResult = check("sinir-buyurgan", model: model.values, screen: careful.screen, keys: keys)
countVerdicts(carefulResult)
let bannerResult = check("sinir-bildirimsel", model: model.values, screen: banner, keys: keys)
countVerdicts(bannerResult)
```

```
sinir-buyurgan | ProfileController.title: model=Ayse ekran=Ayse -> uyumlu
sinir-buyurgan | ProfileController.unread: model=3 ekran=3 -> uyumlu
sinir-buyurgan | ProfileController.status: model=cevrimici ekran=cevrimici -> uyumlu
sinir-buyurgan | ProfileController.banner: model=- ekran=- -> olcum-disi
uyumlu: 3
bayat: 0
eksik: 0
artik: 0
sinir-bildirimsel | BannerProfileView.title: model=Ayse ekran=Ayse -> uyumlu
sinir-bildirimsel | BannerProfileView.unread: model=3 ekran=3 -> uyumlu
sinir-bildirimsel | BannerProfileView.status: model=cevrimici ekran=cevrimici -> uyumlu
sinir-bildirimsel | BannerProfileView.banner: model=- ekran=yeni -> artik
uyumlu: 3
bayat: 0
eksik: 0
artik: 1
```

Buyurgan ekran üç uyumlu verdi ve `banner` ölçüm dışında kaldı. Bildirimsel ekran aynı üç uyumluyu ve bir **artık** verdi. İlk bakışta iki ekrandan biri "daha uyumlu" görünür, ama iki çerçevenin sayısı birbirini sıralamaz: aynı kural, aynı model, kodun yazdığı farklı hatalar.

**KP22.** Model, ekranı kuran çerçeveyi bilmez; yalnız ekranın gösterdiği alanları karşılaştırır. Çerçeve seçiminin gerekçeleri ölçülmez, ve bu derste sayıya dökülmez.

Gerçek gerekçeler başka yerdedir ve hepsi düzyazıyla tartılır.

- **Ekip.** Ekibin bildirimsel çerçeveyi bilmesi, ekran başına yazılan satırın azalması demektir; ekip buyurgan çerçeveyi biliyorsa öğrenme maliyeti yeni çerçevenin getirdiği kazancı aşabilir.
- **Eski kod.** Çalışan bir ekranı yeniden yazmak, risk taşır. Önceki dersteki kademeli yol, bu riski ekran başına küçültür.
- **Platform sürümü.** Bildirimsel çerçeve her hedef sürümde bulunmaz; desteklenecek en düşük sürüm çerçeveyi seçmeden önce belirlenir.
- **Özel çizim.** Elle çizilen ya da katman animasyonlu bir parça, buyurgan çerçevenin sunduğu denetime ihtiyaç duyabilir; bildirimsel ekran onu sarmalayarak alır.

Bu dört gerekçeden hiçbiri bir sayı değildir, ve ölçümümüz bu kararı vermeye yetmez.

#### Özet

- Aynı model iki çerçeveyle kurulunca iki ekran da altı alanda uyumludur (6 uyumlu).
- Buyurgan tarafta yazılmayan alan eksik, modeli izlemeyen alan bayat ölçülür; bildirimsel taraf aynı olaylarda uyumludur (10 uyumlu, 1 bayat, 1 eksik).
- Bozulma iki çerçevede de olur: bildirimsel ekranın modelde olmayan sabit metni artık verir (3 uyumlu, 1 artık).
- Uyum bir çerçeve seçim ölçütü değildir; seçim ekip, eski kod, platform sürümü ve özel çizim üzerinden düzyazıyla tartılır.
- Bu dersin uyumu: **uyumlu**.

#### Kurs Kapanışı

Kurs, tek bir soruyla on beş dersi ölçtü: bu ekran modelle uyuşuyor mu? Her dersin olayı, uyumu ve sınırlayıcı ölçümü aşağıdadır. Uyum hücreleri kaynak dersin özetinden alınmıştır.

| Ders | Olay | Uyum | Sınırlayıcı ölçüm |
|---|---|---|---|
| Görünüm Denetleyicisi Kavramı | yükleme, okundu işareti, klasör değişimi, taslak gelişi, hücre yeniden kullanımı | dördü (3/2/2/2) | biçimlenmiş metin eşit sayılmaz; iki tarafta olmayan alan sayılmaz |
| Yaşam Döngüsü Yöntemleri | yalnız yüklemede yazılan değer, model altta değişir | bayat | çağrı sırasını model kurar, çerçeve değil |
| Gezinme Denetleyicileri | itme ve geri çıkarma, kalkan seçimin vurgusu | artık | çıkarılan ekran ölçüm dışı |
| Sunum Biçimleri | kip sunulan not, sunan ekranın yazmadığı alan | eksik | yalnız görünür ekrana bakılır |
| Görsel Düzenleyici ve Kodla Arayüz | kopuk bağlantı, yer tutucu metin | bayat | yer tutucu kodun yazmadığı etikette kalır |
| Bağlantılar ve Eylemler | modeli yazıp etiketi yazmayan eylem | bayat | biçimlenmiş metin ham değerle bayat çıkar |
| Kısıt Tabanlı Düzen | güncellenmeyen kısıt sabiti | bayat | öncelik ve belirsizlik modelde yok |
| Yığıt Görünümleri | gizlenmeyen alt görünüm | artık | gizleme ile kaldırma aynı değeri yazar |
| Tablo Görünümleri | yeniden kullanılan hücrede kalan ek | artık | kuyruk boyutu ölçülmez |
| Koleksiyon Görünümleri | eklenip bildirilmeyen öğe | eksik | konum hesabı ölçülmez |
| Temsilci Kalıbı | bağlanmamış temsilci | bayat | unutan kişi okunmaz |
| Çizim ve Katman Animasyonu | istenmeyen yeniden çizim | bayat | animasyon süresi ölçülmez |
| Bildirimsel ve Buyurgan Arayüz Bir Arada | atanmayan kök görünüm | bayat | bildirimsel yeniden hesap varsayım |
| Eski Kodun Kademeli Dönüşümü | taşınmayan alan, kalkan rozet | eksik | ekran düzeyinde tek yanıt yok |
| Çerçeve Seçimi | aynı model, unutulan alan | uyumlu | uyum seçim ölçütü değil |

Açılış dersi dışındaki on dört dersin dağılımı: 1 uyumlu, 7 bayat, 3 eksik, 3 artık. Çekirdeğin 3/2/2/2 dağılımı ayrı bir sayımdır: çekirdek açılış dersindeki dokuz alanı sayar, dağılım ise on dört dersin uyumunu sayar; ikisi aynı şey değildir.

Sıradaki kurs (M30/K05) verinin saklanmasını, ağ isteklerini ve eşzamanlılığı konu alır. Bu kursun ölçü ekseni orada sürmez; K05 kendi sorusunu kendisi sorar.

