---
title: 'Görünüm Denetleyicisi Kavramı'
source: 'https://academia.sh/tr/kurslar/uikit/gorunum-denetleyicisi-kavrami'
course: 'UIKit ve Köprüleme'
language: tr
updated: '2026-10-21T05:03:16+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Görünüm denetleyicisinin modeli ekrana ve ekrandaki olayları modele taşıyan nesne olduğunu ayırır'
    - 'Bir ekranın modelle uyuşmasını dört sonuçla sınıflar'
    - 'Beş olayda hangi alanın uyumlu, bayat, eksik ya da artık kaldığını okur'
    - 'Ölçümün sınırını söyler'
---

# Görünüm Denetleyicisi Kavramı

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.
