İçeriğe geç
academia.sh

Ders 17 / 21

Örtük ve Açık Animasyonlar

Animasyonla sarılan bir değişikliğin, sarılmamış aynı değişiklikle aynı görünümleri aynı tetikle yeniden hesaplattığının ölçümü.

Yayın Güncelleme

Bu derste

  • Örtük ve açık animasyonu, değişikliği kimin sardığı üzerinden ayırır
  • Açık animasyonla sarılan yerel değişikliğin yalnız sahibini yeniden hesaplattığını ölçer
  • Örtük animasyonun ebeveynden gelen değeri izlediği durumda tetiğin ebeveyn olduğunu ölçer
  • Animasyonlu ve animasyonsuz değişikliğin modelde aynı sayıldığını gösterir
İçindekiler

Önceki derste sürükleme bittiğinde konumun değiştiğini ve kartın yeni yerinde durduğunu ölçtük. O değişikliğin görünüme ani mi yoksa ara değerlerden geçerek mi yansıyacağı ayrı bir karardır ve o kararı veren araç animasyondur. Soru yine aynı: bu yeniden hesaplamayı ne tetikledi? Bu derste bakılan tetik yerel; yanında bir ebeveyn örneği de vardır.

Animasyon, bir değişikliğin görünüme yansımasını zamana yayan bildirimsel bir tanımdır. İki yolla eklenir. Açık animasyon, değişikliği yapan kodu sarar: withAnimation { isExpanded.toggle() } yazan kişi, “bu değişiklik animasyonla yansısın” demiş olur. Örtük animasyon ise görünüme bağlanır: .animation(_:value:) değiştiricisi, verilen değer değiştiğinde o görünümün altındaki değişikliklere animasyon uygular. İlki değişikliğin bulunduğu yerde, ikincisi değişikliği gören görünümde yazılır. Ama ikisi de bir değeri değiştirmez; yalnız değişikliğin yansıma biçimini seçer.

Ölçüm çekirdek dosyayla yapılır; çekirdek her derste aynı dosya adıyla ve aynı içerikle gelir.

// 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)")
    }
}

Aynı Değişiklik, İki Sarmalama

Aşağıdaki parçada bir panel vardır. Panelin ayrıntı alanı, kendi durumu olan isExpanded ile açılıp kapanır ve düğme bu değişikliği açık animasyonla sarar. Panelin saydamlığı ise ekranın verdiği isOn değerine bağlıdır ve örtük animasyon bu değeri izler.

import SwiftUI

struct PanelScreen: View {
    @State private var isOn = false

    var body: some View {
        VStack {
            Toggle("Etkin", isOn: $isOn)
            DetailPanel(isOn: isOn)
            StatusText()
        }
    }
}

struct DetailPanel: View {
    let isOn: Bool
    @State private var isExpanded = false

    var body: some View {
        VStack {
            Button("Ayrıntı") {
                withAnimation {
                    isExpanded.toggle()
                }
            }
            if isExpanded {
                Text("Ayrıntı metni")
            }
        }
        .opacity(isOn ? 1 : 0.4)
        .animation(.default, value: isOn)
    }
}

struct StatusText: View {
    var body: some View {
        Text("Hazır")
    }
}

Bu parça derlenmez ve çıktı vermez; yalnız kaynak olarak durur. Okuma düzeni şöyledir. PanelScreen, isOn’un sahibidir ve gövdesinde onu hem bağlama olarak hem değer olarak kullanır; yani değeri okur. DetailPanel, isOn’u parametre olarak alır ve saydamlığı için okur, isExpanded’ı ise kendi durumu olarak okur. StatusText hiçbirini okumaz. Bu okuma düzenini düz değerlerle yeniden kuralım. Model, çerçevenin bağımlılık izleme kuralını kurar; çerçevenin kendisi değildir.

İki değişiklik yazalım: panelin açık animasyonla açılması ve ekrandaki isOn değerinin değişmesi. İkincisinde örtük animasyon devrededir.

// main.swift — acik animasyonla sarilan yerel degisiklik ve ortuk animasyonun izledigi ebeveyn degeri
let storage: [String: Storage] = [
    "isOn": .state(owner: "PanelScreen"),
    "isExpanded": .state(owner: "DetailPanel"),
]

let screen = ViewTree(nodes: [
    ViewNode("PanelScreen", parent: nil, reads: ["isOn"]),
    ViewNode("DetailPanel", parent: "PanelScreen", reads: ["isOn", "isExpanded"]),
    ViewNode("StatusText", parent: "PanelScreen", reads: []),
], storage: storage)

let recomputes = [
    screen.change("panel-acildi", key: "isExpanded"),
    screen.change("etkin-degisti", key: "isOn"),
].flatMap { $0 }

countCauses(recomputes)
panel-acildi: isExpanded degisti | yeniden DetailPanel yerel | atlanan PanelScreen,StatusText
etkin-degisti: isOn degisti | yeniden PanelScreen yerel, DetailPanel ebeveyn | atlanan StatusText
yerel: 2
ebeveyn: 1
gozlem: 0
ortam: 0

İlk satır açık animasyonun ölçümüdür. withAnimation içindeki değişiklik, isExpanded’ın sahibi olan DetailPanel’i yeniden hesaplatır; tetik yereldir. Ekran ve durum metni atlanır. Animasyon bu sayıyı değiştirmedi, çünkü sarmalayıcı hangi görünümün okuduğunu belirlemez, yalnız yansımanın nasıl olacağını söyler.

İkinci satır örtük animasyonun ölçümüdür. isOn değişince sahibi PanelScreen yerel tetikle yeniden hesaplanır, DetailPanel ise değeri ebeveynden aldığı için ebeveyn tetikle. Örtük animasyonu taşıyan görünüm, değişikliği yapan görünüm değildir: değiştiriciyi DetailPanel taşır, değer ise ekrandadır. StatusText yine atlanır. Son sayım, iki yerel ve bir ebeveyn yeniden hesaplamayı verir; gözlem ve ortam sıfırdır.

EK5. Bu ölçüm yalnız isExpanded’ın DetailPanel’in kendi durumu ve isOn’un ekranın durumu olduğu ağaç için geçerlidir. Bu iki değerin aynı sahibe ait olduğu bir düzen ölçülmedi.

EK6. Bu ölçüm, DetailPanel gövdesinin isOn’u okuduğunu varsayar. Değiştiricinin izlediği değerin gövdede okunmadığı bir yazım ölçülmedi.

Animasyon Bir Tetik Değildir

Bu iki satır, dersin merkezindeki ayrımı gösterir. Açık animasyon bir değişikliği sarar, örtük animasyon bir değeri izler; ikisi de tetik üretmez. Tetik, hâlâ değerin kendi saklama yoludur: yerel durum, ebeveynden gelen değer, gözlemlenen nesne ya da ortam değeri. Animasyon bu dört kanalın hiçbirine beşinci bir kanal eklemez.

Bu ayrımın pratik sonucu hata ayıklarken ortaya çıkar. Bir görünümün beklenmedik biçimde yeniden hesaplandığı görülürse, animasyonu suçlamadan önce hangi değerin değiştiğine bakılır. Animasyonu kaldırmak yeniden hesaplamayı kaldırmaz; yalnız ara değerleri kaldırır. Öte yandan örtük animasyonun hangi değere bağlandığı önemlidir: value: parametresi, hangi değişikliğin animasyonla yansıyacağını seçer. Değer parametresi olmayan biçim, animasyonu görünümün altındaki her değişikliğe uygular ve kullanımdan kaldırılmıştır; bu yüzden animasyon, value: parametresiyle bir değere bağlanarak yazılır.

Açık animasyonun avantajı ise kapsamıdır. withAnimation bloğunun içinde yapılan değişiklikler o animasyonla yansır; bloğun dışında yapılan bir değişiklik bu animasyonu almaz, başka bir animasyon kaynağı yoksa animasyonsuz yansır. Aynı değer iki farklı yerden yazılıyorsa biri animasyonlu, öteki animasyonsuz olabilir. Model bu iki yazımı ayırmaz.

Bir kod incelemesinde bu ayrım üç soruya dönüşür. Birincisi, değer kimin: değeri tutan görünüm, değiştiren görünümle aynı mı? Aynıysa tetik yereldir ve animasyon ister açık ister örtük olsun yalnız o görünümü ilgilendirir. İkincisi, değeri kim okuyor: ebeveynden parametre ya da bağlama ile alan görünümler ebeveyn tetikle yeniden hesaplanır ve örtük animasyonu taşıyan görünüm çoğu zaman bunlardan biridir. Üçüncüsü, animasyon nerede yazılmış: değişikliği yapan kodda mı, değeri izleyen görünümde mi? Bu üçüncü soru ölçüm sonucunu değiştirmez, ama okuyanın niyeti bulmasını kolaylaştırır. Bir withAnimation bloğu, değişikliğin bilerek animasyonlandığını; bir .animation(_:value:) değiştiricisi ise görünümün bu değere her zaman animasyonla tepki vermesi istendiğini söyler.

Örnekteki StatusText iki satırda da atlandı. Bu, animasyonlu bir ekranın her görünümünün hareket ettiği anlamına gelmez; yalnız değeri okuyan görünüm yeniden hesaplanır. Okumayan görünüm, kardeşinin animasyonundan etkilenmeden olduğu gibi kalır.

EK7. Bu ölçüm animasyonun bir tetik üretmediğini, yalnız iki değişiklik için gösterir. Animasyonun bitiş anında çerçevenin bir ek hesaplama yapıp yapmadığı ölçülmedi.

EK8. Bu ölçüm süre, eğri ve kare sıklığı hakkında bir şey söylemez. Bu üç değer ne basıldı ne de karşılaştırıldı.

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

Aynı anahtarı iki kez yazalım; birinde değişiklik animasyonla sarılıdır, ötekinde değil. Model, bu farkı kuracak bir alan taşımaz. Bu bilinçli bir sınırdır: çekirdeğin change işlevi yalnız anahtarı ve etiketi alır.

// main.swift — animasyonlu ve animasyonsuz ayni degisiklik
let storage: [String: Storage] = [
    "isExpanded": .state(owner: "DetailPanel"),
]

let screen = ViewTree(nodes: [
    ViewNode("DetailPanel", parent: nil, reads: ["isExpanded"]),
    ViewNode("StatusText", parent: nil, reads: []),
], storage: storage)

let animated = screen.change("animasyonlu", key: "isExpanded")
let plain = screen.change("animasyonsuz", key: "isExpanded")
print(animated.map(\.view) == plain.map(\.view))
print(animated.map(\.cause) == plain.map(\.cause))
animasyonlu: isExpanded degisti | yeniden DetailPanel yerel | atlanan StatusText
animasyonsuz: isExpanded degisti | yeniden DetailPanel yerel | atlanan StatusText
true
true

İki değişiklik aynı satırı verir ve iki karşılaştırma da doğrudur: aynı görünüm, aynı tetik. Model animasyonu sayamaz; sayması da gerekmez, çünkü bu kursun ölçüsü yeniden hesaplamanın nedenidir, görünümün ara değerleri değil. Animasyonun ekranda nasıl göründüğü, ne kadar sürdüğü ve ara karelerde gövdenin yeniden çağrılıp çağrılmadığı ölçülmedi.

Özet

  • Açık animasyon değişikliği, örtük animasyon görünümü sarar; ikisi de değeri değiştirmez, yansımanın biçimini seçer.
  • Açık animasyonla sarılan yerel değişiklik, değerin sahibini yerel tetikle yeniden hesaplatır.
  • Örtük animasyonun izlediği değer ebeveynden geliyorsa, o görünüm ebeveyn tetikle yeniden hesaplanır.
  • Animasyonlu ve animasyonsuz aynı değişiklik modelde aynı görünümleri aynı tetikle sayar.
  • Bu dersin tetiği: yerel.

Sonraki Adım

Bu derste değişiklik hep bir görünümü yeniden hesaplattı; görünüm ağaçta kaldı. Sonraki derste bir görünümün ağaca eklenmesini ve ağaçtan çıkarılmasını, yani geçişi ölçeceğiz. Orada bir modelden silinen öğenin hangi görünümü yeniden hesaplattığına, kalkan görünümün ise neden sayılmadığına 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