İçeriğe geç
academia.sh

Ders 13 / 15

Bildirimsel ve Buyurgan Arayüz 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ü.

Yayın Güncelleme

Bu derste

  • İki çerçevenin bir ekranda birleştiği iki yönü, gömme ve sarmalama, birbirinden ayırır
  • Barındırma denetleyicisinde kök görünüm yeniden atanmadığında alanın eski değerde kaldığını ölçer
  • Sarmalamada güncelleme yöntemine yazılan çağrının alanı modele eşitlediğini doğrular
  • Bildirimsel tarafın kendi kendine yeniden hesaplamasının bu kuralla görülmediğini adlandırır
İçindekiler

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

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

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.

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

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

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

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.

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

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.

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

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

İ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