İçeriğe geç
academia.sh

Ders 06 / 15

Bağlantılar 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ü.

Yayın Güncelleme

Bu derste

  • Bağlantı ile eylemin hangi yönde çalıştığını ayırır
  • Eylemin modeli güncelleyip etiketi yazmadığı durumu bayat olarak ölçer
  • Hem modeli hem etiketi yazan eylemin uyumlu olduğunu gösterir
  • Biçimlendirilmiş metni ham değerle karşılaştırmanın neden bayat çıktığını açıklar
İçindekiler

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

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:

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.

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

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

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:

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.

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

İ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