---
title: 'Gezinme Denetleyicileri'
source: 'https://academia.sh/tr/kurslar/uikit/gezinme-denetleyicileri'
course: 'UIKit ve Köprüleme'
language: tr
updated: '2026-10-21T05:03:16+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Gezinme denetleyicisinin ekranları bir yığıtta tuttuğunu ve itme ile geri çıkarmanın yığıtı nasıl değiştirdiğini anlatır'
    - 'Geri çıkarılan ekranın altındaki ekranın kendiliğinden güncellenmediğini ölçer'
    - 'Modelden kalkan bir değerin ekranda artık olarak kalmasını tanır'
    - 'İtilen ekranın ilk yazmasını bir sapma saymayan ölçüm sınırını ayırır'
---

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