İçeriğe geç
academia.sh

Ders 02 / 15

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

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

Yayın Güncelleme

Bu derste

  • Görünüm denetleyicisinin yaşam döngüsü yöntemlerini ve her birinin ne zaman çağrıldığını sıralar
  • Yalnız bir kez çağrılan yönteme yazılan değerin ekranı nasıl bayat bıraktığını ölçer
  • Yazma yerini her görünmede çağrılan yönteme taşımanın ekranı modelle uyumlu tuttuğunu gösterir
  • Modelin çağrı sırasının dersin seçimi olduğunu, gerçek sıranın ölçülmediğini ayırır
İçindekiler

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

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.

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.

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

// 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; InvoiceControllerin 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.

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

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat