İçeriğe geç
academia.sh

Ders 13 / 21

Gezinme Yığıtı ve Yolu

Gezinme yolunun kökte tutulan bir durum olduğunun, itme ve geri dönmenin aynı tetiği ürettiğinin ve yola itilen değerin bir görünüm olmadığının ölçümü.

Yayın Güncelleme

Bu derste

  • Gezinme yolunun kökte tutulan yerel bir durum olduğunu ölçüyle gösterir
  • Yola ekran itmenin ve geri dönmenin aynı yeniden hesaplama kümesini ürettiğini ayırır
  • Hedef görünümün gösterdiği nesne özelliğinin gözlem tetiği ürettiğini ölçer
  • Yola itilen değerin görünüm olmadığını, hedefin yeni bir düğüm olarak ağaca girdiğini açıklar
İçindekiler

Önceki dört ders durumun nerede yaşadığına baktı: görünümde, bir bağlamanın ucunda, bir nesnede ve ortamda. Bu derste durumun gösterdiği şey değişir. Ekranlar arasında ilerlemek, hangi ekranın görüneceğini söyleyen bir değerin değişmesidir ve bu değer de yukarıdaki saklama yerlerinden birinde durur. Gezinme yığıtı (navigation stack), kullanıcının açtığı ekranları üst üste tutan yapıdır; yol ise bu yığıtın içeriğini tarif eden değerler dizisidir. Bu derste bakılan tetik yerel: yolu tutan kök görünüm değerin sahibi.

Ölçüm çekirdek dosyayla yapılır.

// Invalidation.swift — bir degisikligin hangi gorunumu hangi kaynak yoluyla yeniden hesaplattigini siniflandirir
enum Storage {
    case state(owner: String)
    case model(object: String)
    case environment
}

struct ViewNode {
    let name: String
    let parent: String?
    let reads: Set<String>

    init(_ name: String, parent: String?, reads: Set<String>) {
        self.name = name
        self.parent = parent
        self.reads = reads
    }
}

struct Recompute {
    let view: String
    let cause: String
}

struct ViewTree {
    let nodes: [ViewNode]
    let storage: [String: Storage]

    func ancestors(of name: String) -> [String] {
        var found: [String] = []
        var current = nodes.first { $0.name == name }?.parent
        while let next = current {
            found.append(next)
            current = nodes.first { $0.name == next }?.parent
        }
        return found
    }

    func cause(of node: ViewNode, key: String) -> String {
        guard let kind = storage[key] else { return "olcum-disi" }
        switch kind {
        case .environment:
            return "ortam"
        case .model:
            return "gozlem"
        case .state(let owner):
            if owner == node.name { return "yerel" }
            if ancestors(of: node.name).contains(owner) { return "ebeveyn" }
            return "kural-disi"
        }
    }

    func change(_ label: String, key: String) -> [Recompute] {
        let hits = nodes.filter { $0.reads.contains(key) }
            .map { Recompute(view: $0.name, cause: cause(of: $0, key: key)) }
        let skipped = nodes.filter { !$0.reads.contains(key) }.map(\.name)
        let recomputed = hits.isEmpty ? "yok" : hits.map { "\($0.view) \($0.cause)" }.joined(separator: ", ")
        let rest = skipped.isEmpty ? "yok" : skipped.joined(separator: ",")
        print("\(label): \(key) degisti | yeniden \(recomputed) | atlanan \(rest)")
        return hits
    }
}

func countCauses(_ recomputes: [Recompute]) {
    for cause in ["yerel", "ebeveyn", "gozlem", "ortam"] {
        print("\(cause): \(recomputes.filter { $0.cause == cause }.count)")
    }
}

Çekirdekte gezinmeye özgü bir tip yoktur. Yol, Storage.state(owner:) ile bir görünümün durumu olarak yazılır; yolu okuyan görünümler reads içinde path anahtarını taşır. Model, çerçevenin bağımlılık izleme kuralını kurar; çerçevenin kendisi değildir.

Yol Kökte Durur

Bir gezinme yığıtı iki parçadan oluşur. Birincisi kökteki NavigationStack: yol durumunu ona bir bağlamayla veririz. İkincisi yoldaki her değerin hangi görünüme karşılık geldiğini söyleyen .navigationDestination(for:destination:) değiştiricisi. Aşağıdaki kaynak parçada yol bir tamsayı dizisidir ve her tamsayı bir öğenin kimliğidir.

import SwiftUI

struct RootScreen: View {
    @State private var path: [Int] = []
    @State private var store = ItemStore()

    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                ItemList(path: $path)
                SettingsButton()
            }
            .navigationDestination(for: Int.self) { itemID in
                ItemDetail(itemID: itemID, store: store, path: $path)
            }
        }
    }
}

struct ItemList: View {
    @Binding var path: [Int]

    var body: some View {
        Button("Ac, derinlik \(path.count)") {
            path.append(7)
        }
    }
}

Yol RootScreen’e aittir. Gövde yolu NavigationStack’e bir bağlamayla verir ve alt görünümlere de aynı bağlamayı iletir. ItemList bu bağlama üzerinden yolu hem okur hem yazar; derinliği düğmenin yazısında gösterir. SettingsButton ise yola dokunmaz. Ağaç, kaynak parçadaki tip adlarını ve özellik adını taşır. İki değişiklik verilir: bir ekranın itilmesi ve geri dönülmesi.

// main.swift — gezinme yolu kokte durur, itme ve geri donme ayni tetigi uretir
let storage: [String: Storage] = [
    "path": .state(owner: "RootScreen"),
]

let screen = ViewTree(nodes: [
    ViewNode("RootScreen", parent: nil, reads: ["path"]),
    ViewNode("ItemList", parent: "RootScreen", reads: ["path"]),
    ViewNode("SettingsButton", parent: "RootScreen", reads: []),
], storage: storage)

let recomputes = [
    screen.change("ekran-itildi", key: "path"),
    screen.change("geri-donuldu", key: "path"),
].flatMap { $0 }

countCauses(recomputes)
ekran-itildi: path degisti | yeniden RootScreen yerel, ItemList ebeveyn | atlanan SettingsButton
geri-donuldu: path degisti | yeniden RootScreen yerel, ItemList ebeveyn | atlanan SettingsButton
yerel: 2
ebeveyn: 2
gozlem: 0
ortam: 0

İki değişiklik, dört yeniden hesaplama. İki satır birebir aynı: RootScreen yerel, ItemList ebeveyn, SettingsButton atlandı. İtmeyi bir düğmeye dokunan kullanıcı, geri dönmeyi sistemin geri hareketi başlatır; değişikliği yazan her seferinde farklı, ama yolun sahibi aynı, bu yüzden sonuç da aynı. Bu, ikinci dersin kuralının bir uygulamasıdır: kural yazanı okumaz. Sayımda yerel iki, ebeveyn iki, gözlem ve ortam sıfır.

Yolun kökte tutulması bir tasarım kararıdır. Yol bir alt görünümde tutulsaydı, kökün ve yolu okumayan kardeşlerin ona erişimi olmazdı; durumu ataya kaldırmanın nedeni budur. Gezinme yolu en baştan kökte durur, çünkü hangi ekranın gösterileceği kararı tüm ekranlara bağlıdır.

DR13. Bu ölçüm iki değişiklik ve tek bir yol anahtarı için. Yolun birden çok değerinin tek seferde değiştiği durum, örneğin tüm yığıtın boşaltılması, ölçülmedi. NavigationStack’in yola yazılan değere karşılık gelen hedefi nasıl bulduğu da ölçülmedi.

Hedefin Gösterdiği Nesne

Yol bir değer dizisidir: tamsayıdır, görünüm değildir. Yola bir değer itildiğinde çerçeve .navigationDestination ile tanımlanan kapanışı o değerle çağırır ve ortaya çıkan görünümü yığıta koyar. Hedef görünüm gerektiğinde bir nesnenin özelliğini de okur. Aşağıdaki kaynak parçada hedef, öğenin başlığını bir gözlemlenebilir nesneden okuyan ItemDetail olsun; yol ona bağlama olarak da iletiliyor.

import SwiftUI
import Observation

@Observable
final class ItemStore {
    var itemTitle = "Taslak"
}

struct ItemDetail: View {
    let itemID: Int
    let store: ItemStore
    @Binding var path: [Int]

    var body: some View {
        VStack {
            Text("\(itemID): \(store.itemTitle)")
            Button("Derinlik \(path.count)") { path.append(itemID + 1) }
        }
    }
}

ItemDetail iki şey okur: yolun uzunluğunu ve nesnenin başlığını. Bu hedef ekranı artık açıktır ve yığıtta durmaktadır; ağaç RootScreen, ItemList ve ItemDetail üçlüsünü kurar. ItemDetail kökün altında yer alır, çünkü hedef kapanışını kök kurar.

// main.swift — hedefin gosterdigi nesne gozlem, hedefin yolu okumasi ebeveyn uretir
let storage: [String: Storage] = [
    "path": .state(owner: "RootScreen"),
    "itemTitle": .model(object: "ItemStore"),
]

let screen = ViewTree(nodes: [
    ViewNode("RootScreen", parent: nil, reads: ["path"]),
    ViewNode("ItemList", parent: "RootScreen", reads: ["path"]),
    ViewNode("ItemDetail", parent: "RootScreen", reads: ["path", "itemTitle"]),
], storage: storage)

let recomputes = [
    screen.change("baslik-degisti", key: "itemTitle"),
    screen.change("ikinci-ekran-itildi", key: "path"),
].flatMap { $0 }

countCauses(recomputes)
baslik-degisti: itemTitle degisti | yeniden ItemDetail gozlem | atlanan RootScreen,ItemList
ikinci-ekran-itildi: path degisti | yeniden RootScreen yerel, ItemList ebeveyn, ItemDetail ebeveyn | atlanan yok
yerel: 1
ebeveyn: 2
gozlem: 1
ortam: 0

baslik-degisti yalnız ItemDetail’ı yeniden hesaplattı ve gözlem: başlık bir nesne özelliğidir ve yolu tutan kökle ilgisi yoktur. ikinci-ekran-itildi üç görünümü birden yeniden hesaplattı: kök yerel, ItemList ve ItemDetail ebeveyn. Hedef ekran artık yığıtta olduğu için yolu okuyor ve yol değiştiğinde yeniden hesaplanıyor. Sayılar: yerel bir, ebeveyn iki, gözlem bir.

Burada iki tetik aynı görünümde karşılaşır. ItemDetail aynı ölçümde bir kez gözlem, bir kez ebeveyn tetiği aldı. Hedef görünümün hangi değişikliği yaşadığı, onun hangi değeri okuduğuna ve değerin nerede saklandığına bağlıdır; gezinme bunu değiştirmez.

DR14. Bu ölçüm yolu okuyan bir hedef ve tek bir nesne özelliği için. Hedefin yolu hiç okumadığı durumda yol değişince yeniden hesaplanmayacağı, ölçülmeden yalnız modelin kuralından çıkar; hedef kapanışının yolu okuyup okumadığı ölçülmedi.

Sınırlayıcı Ölçüm

Yola itilen şey bir görünüm değil, bir değerdir. Yolu değiştirmek bir görünümü ağaca eklemez; çerçeve değere karşılık gelen hedefi kurar. Bu yüzden hedef ilk göründüğü anda yeni bir düğümdür ve o ilk görünüşü bir yeniden hesaplama değildir. Yeniden hesaplama, daha önce var olan bir görünümün bir değer değişince gövdesinin yeniden okunmasıdır. Aşağıdaki ölçüm aynı değişikliği iki ağaçta uygular: hedefin henüz yığıtta olmadığı ve olduğu.

// main.swift — yola itilen deger gorunum degildir, hedef yeni dugumdur
let storage: [String: Storage] = [
    "path": .state(owner: "RootScreen"),
]

let beforePush = ViewTree(nodes: [
    ViewNode("RootScreen", parent: nil, reads: ["path"]),
    ViewNode("ItemList", parent: "RootScreen", reads: ["path"]),
], storage: storage)

let afterPush = ViewTree(nodes: [
    ViewNode("RootScreen", parent: nil, reads: ["path"]),
    ViewNode("ItemList", parent: "RootScreen", reads: ["path"]),
    ViewNode("ItemDetail", parent: "RootScreen", reads: ["path"]),
], storage: storage)

let recomputes = [
    beforePush.change("hedef-yokken-itildi", key: "path"),
    afterPush.change("hedef-varken-itildi", key: "path"),
].flatMap { $0 }

countCauses(recomputes)
hedef-yokken-itildi: path degisti | yeniden RootScreen yerel, ItemList ebeveyn | atlanan yok
hedef-varken-itildi: path degisti | yeniden RootScreen yerel, ItemList ebeveyn, ItemDetail ebeveyn | atlanan yok
yerel: 2
ebeveyn: 3
gozlem: 0
ortam: 0

İlk satırda ItemDetail ne yeniden hesaplananlar arasında yer alıyor ne de atlananlar arasında. Ağaçta o anda böyle bir düğüm yoktu; itme onu oluşturacak, ama model yeni oluşan düğümün ilk gövdesini bir tetik olarak saymaz. İkinci satırda hedef zaten yığıttaydı ve yolu okuyordu, bu yüzden ebeveyn tetikle yeniden hesaplandı. Sayımda yerel iki, ebeveyn üç; ilk satırın itme eylemi hedefin ilk görünüşünü saymadı.

Bu ayrım ders için bir uyarıdır: gezinmede “ekran açıldı” ile “ekran yeniden hesaplandı” aynı olay değildir. Ekranın ilk açılışı bir kuruluştur; yeniden hesaplama ancak ekran var olduktan sonra, okuduğu değer değişince olur.

DR15. Bu ölçüm hedefin var olmadığı ve olduğu iki durumu karşılaştırır. Hedefin ilk gövde hesabının çerçevede kaç adımda yapıldığı ve hedefin yığıttan çıkarılıp yeniden kurulmasında durumunun korunup korunmadığı ölçülmedi.

Özet

  • Gezinme yolu, kökte tutulan bir durumdur; sahibi kök görünümdür.
  • Yol değişince yolu tutan kök yerel, yolu okuyan alt görünümler ebeveyn tetiğiyle yeniden hesaplanır.
  • Ekran itmek ile geri dönmek aynı yeniden hesaplama kümesini üretir; yazanın kim olduğu tetiği değiştirmez.
  • Hedef görünümün gösterdiği nesne özelliği gözlem, okuduğu yol ebeveyn tetiği üretir.
  • Yola itilen şey görünüm değil değerdir; hedef yeni bir düğümdür ve ilk görünüşü yeniden hesaplama sayılmaz.
  • Bu dersin tetiği: yerel.

Sonraki Adım

Bu derste yol bir ekranı yığıta ekledi ve ekran yığıtın bir parçası oldu. Her ekran yığıta girmez: bazıları mevcut ekranın üzerine geçici olarak açılır ve iş bitince kapanır. Sıradaki derste bir sayfa, bir uyarı ve bir tam ekran tabaka da yine bir değere bağlıdır; sunum durumu ve kapatma, yola benzer ama başka bir sahiplikle çalışır.

İ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