Ders 16 / 21
Hareketler
Sürükleme sırasındaki geçici değerin yerel, hareket bitince yazılan kalıcı konumun ebeveyn tetiğiyle yeniden hesaplamaya yol açtığının ölçümü.
Yayın Güncelleme
Bu derste
- Hareketin geçici değerini hareket durumunda, kalıcı sonucunu durumda tutmayı ayırt eder
- Sürükleme sırasındaki değişikliğin yalnız hareketi taşıyan görünümü yeniden hesaplattığını ölçer
- Hareket bitince yazılan konumun okuyan kardeşlere ebeveyn tetiğiyle ulaştığını ölçer
- Hareket durumunun ilk değerine dönüşünün ayrı bir değişiklik olarak sayıldığını gösterir
İçindekiler
Önceki konuda bir değerin hangi yoldan görünüme bağlandığına baktık; kullanıcı bu değerleri çoğunlukla bir hareketle değiştirir. Bu derste soru değişmez: bu yeniden hesaplamayı ne tetikledi? Bu derste bakılan tetik yerel. Kursun açılış dersinde altı değişiklik sekiz yeniden hesaplama üretmişti; ikisi yerel, ikisi ebeveyn, ikisi gözlem, ikisi ortam tetiğine aitti. Şimdi tek bir sürüklemenin bu dört kategoriden hangilerini kullandığını sayacağız.
Hareket (gesture), kullanıcının ekrana dokunuşunu bir değere çeviren bildirimsel bir tanımdır: dokunma, uzun basma, sürükleme ve bunların birleşimleri aynı biçimde görünüme bir değiştirici olarak eklenir. Sürüklemenin bir özelliği ölçüyü belirler. Parmak ekrandayken konum saniyede birçok kez değişir, ama parmak kalkınca bu ara değerlerin hiçbiri kalıcı olmak zorunda değildir. Çerçeve bunun için ayrı bir özellik sarmalayıcısı sunar: @GestureState. Hareket sürerken değer güncellenir, hareket bitince değer kendiliğinden ilk değerine döner. Kalıcı sonuç ise hareketin bitiş geri çağrısında, sıradan bir durum değerine yazılır.
Ö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)")
}
}
Sürüklemenin İki Değeri
Aşağıdaki parça bir kartı sürükler. Kartı çizen görünüm geçici ofseti kendi hareket durumunda tutar. Kalıcı konum ise bir üst görünümdedir ve karta bağlama ile verilir; konumu bir etiket de okur.
import SwiftUI
struct BoardScreen: View {
@State private var position = CGSize.zero
var body: some View {
VStack {
DragCard(position: $position)
PositionLabel(position: position)
HintText()
}
}
}
struct DragCard: View {
@Binding var position: CGSize
@GestureState private var dragOffset = CGSize.zero
var body: some View {
RoundedRectangle(cornerRadius: 12)
.offset(
x: position.width + dragOffset.width,
y: position.height + dragOffset.height
)
.gesture(
DragGesture()
.updating($dragOffset) { value, state, _ in
state = value.translation
}
.onEnded { value in
position.width += value.translation.width
position.height += value.translation.height
}
)
}
}
struct PositionLabel: View {
let position: CGSize
var body: some View {
Text("\(Int(position.width)), \(Int(position.height))")
}
}
struct HintText: View {
var body: some View {
Text("Kartı sürükleyin")
}
}
Bu parça derlenmez ve çıktı vermez; platform çerçevesini içe aldığı için yalnız kaynak olarak durur. Ağacı buradan okuyup düz değerlerle yeniden kuracağız. DragCard iki değeri okur: dragOffset kendi hareket durumudur, position ise ebeveynden gelen bağlamadır. PositionLabel yalnız position’ı okur. HintText hiçbirini okumaz. BoardScreen konumun sahibidir ve gövdesinde onu bağlama olarak ve değer olarak iletir; yani gövdesi de konumu okur.
Ölçmeden önce modelin ne olduğunu söyleyelim. Model, çerçevenin bağımlılık izleme kuralını kurar, çerçevenin kendisi değildir: bir görünüm okuduğu değer değişince yeniden hesaplanır, okumadığı değer değişince atlanır. Çerçevenin gövdeyi kaç kez çağırdığı ya da ne zaman çizdiği burada ölçülmez.
Çekirdek bir şey değiştirmez; bir anahtar verildiğinde onu okuyanları bulur ve her biri için tetiği sınıflandırır. Şimdi yukarıdaki kaynak parçadaki tip adlarıyla aynı adları taşıyan ağacı kuralım ve üç değişiklik yazalım: sürükleme sürerken dragOffset, hareket bitince position ve hareket durumunun ilk değerine dönüşü.
// main.swift — surukleme sirasinda ve sonrasinda uc degisiklik
let storage: [String: Storage] = [
"position": .state(owner: "BoardScreen"),
"dragOffset": .state(owner: "DragCard"),
]
let screen = ViewTree(nodes: [
ViewNode("BoardScreen", parent: nil, reads: ["position"]),
ViewNode("DragCard", parent: "BoardScreen", reads: ["position", "dragOffset"]),
ViewNode("PositionLabel", parent: "BoardScreen", reads: ["position"]),
ViewNode("HintText", parent: "BoardScreen", reads: []),
], storage: storage)
let recomputes = [
screen.change("surukleme-suruyor", key: "dragOffset"),
screen.change("surukleme-bitti-konum", key: "position"),
screen.change("surukleme-bitti-ofset", key: "dragOffset"),
].flatMap { $0 }
countCauses(recomputes)
surukleme-suruyor: dragOffset degisti | yeniden DragCard yerel | atlanan BoardScreen,PositionLabel,HintText surukleme-bitti-konum: position degisti | yeniden BoardScreen yerel, DragCard ebeveyn, PositionLabel ebeveyn | atlanan HintText surukleme-bitti-ofset: dragOffset degisti | yeniden DragCard yerel | atlanan BoardScreen,PositionLabel,HintText yerel: 3 ebeveyn: 2 gozlem: 0 ortam: 0
Sürükleme sürerken tek görünüm yeniden hesaplanır: kartın kendisi, çünkü dragOffset onun hareket durumudur ve yalnız onun gövdesi okur. İki kardeşi ve ebeveyni atlanır. Bu, sürüklemenin ucuz olmasının nedenidir: parmak her kıpırdadığında etiketin ya da ipucu metninin gövdesi yeniden hesaplanmaz.
EK1. Bu ölçüm yalnız dragOffset değerinin sahibinin DragCard olduğu ağaç için geçerlidir. Hareket durumunu bir üst görünüme taşıyan bir düzenin sayıları farklı olurdu; o düzen ölçülmedi.
EK2. Bu ölçüm sürükleme sırasında tek bir değişikliği temsil eder. Parmağın bir saniyede kaç ara değer ürettiği ve çerçevenin bu değerleri birleştirip birleştirmediği ölçülmedi.
Kalıcı Konum Ebeveyne Yazılır
İkinci satır farklı bir hikâye anlatır. Hareket bitince kalıcı konum position’a yazılır ve bu değerin sahibi BoardScreen’dir. Sahip kendisi yeniden hesaplanır, yani tetik yereldir. Değeri bağlama ile okuyan DragCard ve değeri parametre olarak alan PositionLabel ise sahibin altındadır, yani onlar için tetik ebeveyndir: değişikliği kendileri yapmadılar, bir üst görünümün değeri değişti. HintText yine atlanır, çünkü hiçbir şey okumaz.
Üçüncü satır, hareket durumunun ilk değerine dönüşüdür. Çerçeve bu dönüşü bizim adımıza yapar, ama model yapmaz: dönüş, modelde yazar tarafından ayrı bir değişiklik olarak eklendi. Kart bu değişiklikte bir kez daha yeniden hesaplanır ve tetik yine yereldir. Bu yüzden son sayım yerel tetik için üç, ebeveyn tetiği için iki yeniden hesaplama verir: üç yerelin ikisi kartındır, biri ekranındır; iki ebeveynin ikisi de konum değişikliğinden gelir. Gözlem ve ortam sayıları sıfırdır, çünkü bir sürükleme bu iki kanalı hiç kullanmaz. Açılış dersindeki 2/2/2/2 dağılımı ile bu dağılımın farkı, değişikliğin türünden gelir: tek bir etkileşim çoğunlukla tek bir ya da iki kanalın içinde kalır.
Bu ayrımın kullanım sonucu şudur. Parmakla birlikte hareket eden değer hareket durumunda tutulursa, o değeri okumayan kardeşler sürükleme boyunca etkilenmez. Aynı değer doğrudan position üzerinde güncellenseydi, her ara değer etiketi de yeniden hesaplatırdı. İki değeri ayırmak, çerçevenin sunduğu araca uyan bir tasarımdır ve ölçüm bunu sayıyla gösterir.
EK3. Bu ölçüm, DragCard, PositionLabel ve HintText görünümlerinin BoardScreen’in doğrudan çocukları olduğu ağaç için geçerlidir. Okuyanların daha derin bir ağaca yerleştiği düzen ölçülmedi.
EK4. Bu ölçüm hareketin bitişinde position değerinin bir kez yazıldığını varsayar. Bitiş geri çağrısının hangi anda ve hangi iş parçacığında koştuğu ölçülmedi.
Dokunma ve Birleşik Hareketler
Sürükleme bu ailenin yalnız bir üyesidir. Dokunma, uzun basma ve sürükleme ayrı hareket tanımlarıdır; bir görünüme tek tek eklenebilir ya da birleştirilebilir. Birleştirmenin üç biçimi vardır: biri aynı anda çalışan hareketleri, biri sırayla çalışan hareketleri, öteki ise yalnız birinin başarılı olduğu ve ilkine öncelik veren hareketleri tanımlar. Aşağıdaki parça, önce uzun basılmasını sonra sürüklenmesini isteyen bir sıra kurar ve ayrıca dokunmayı bir durum değerine bağlar.
import SwiftUI
struct CardRow: View {
@State private var isPinned = false
var body: some View {
Text("Kart")
.onTapGesture {
isPinned.toggle()
}
.gesture(
LongPressGesture()
.sequenced(before: DragGesture())
)
}
}
Bu parçada yeni bir tetik kategorisi yoktur. Dokunma kapanışı sıradan bir durum değerine yazar; değerin sahibi CardRow olduğu için değişiklik yereldir ve isPinned’i okuyan başka bir görünüm yoksa yalnız satırın kendisi yeniden hesaplanır. Hareketin kendisi bir tetik değildir: tetik, hareketin sonunda yazılan ya da hareket sırasında güncellenen değerdir. Bu ayrım dersin ana fikridir ve ölçümün neden dragOffset ve position anahtarlarıyla yapıldığını açıklar. Modelde “sürükleme” adlı bir anahtar yoktur; anahtarlar, hareketin dokunduğu saklama yerleridir.
Sıralı bir birleşimde ilk hareket başarılı olmadan ikincisi başlamaz. Bu davranış çerçevenin hareket tanıma kurallarına aittir ve bu derste ölçülmez; model yalnız, birleşimin sonunda hangi değerin yazıldığını sorar.
Sınırlayıcı Ölçüm
Modelin neyi ölçemediğini ayrı bir blokta gösterelim. Hareketin sunduğu her bilgi bir saklama yerine bağlanmaz: sürükleme hızı gibi bir değer yalnız geri çağrının içinde okunur ve hiçbir durumda tutulmaz. Modelde böyle bir değerin saklama kaydı yoktur. Okuyan bir görünüm tanımlayıp kaydı vermezsek çekirdek, tetiği sınıflandıramaz.
// main.swift — saklama kaydi olmayan bir anahtar
let storage: [String: Storage] = [
"position": .state(owner: "BoardScreen"),
]
let screen = ViewTree(nodes: [
ViewNode("BoardScreen", parent: nil, reads: ["position"]),
ViewNode("DragCard", parent: "BoardScreen", reads: ["position", "velocity"]),
], storage: storage)
let recomputes = screen.change("hiz-okundu", key: "velocity")
print(recomputes.count)
hiz-okundu: velocity degisti | yeniden DragCard olcum-disi | atlanan BoardScreen 1
Çekirdek olcum-disi yazar ve bu, bir tetik kategorisi değildir; modelin “bu değeri nereden okuyorsun, bilmiyorum” deme biçimidir. Dersin sayıları yalnız dört kategoriden birine düşen değişiklikler için geçerlidir. Hareket durumunun ilk değerine ne zaman döndüğü de aynı sınıftadır: model dönüşü yazar eklediği için sayar, çerçevenin zamanlamasını iddia etmez. Dönüşün bitiş geri çağrısından önce mi sonra mı olduğu ölçülmedi.
Özet
- Hareket, kullanıcının ekrana dokunuşunu bir değere çeviren bildirimsel bir tanımdır; geçici değer hareket durumunda, kalıcı sonuç durumda tutulur.
- Sürükleme sürerken değişen hareket durumu yalnız onu okuyan görünümü yeniden hesaplatır; kardeşler atlanır.
- Hareket bitince yazılan konum sahibine yerel, onu bağlama ya da parametreyle okuyan çocuklarına ebeveyn tetiğiyle ulaşır.
- Hareket durumunun ilk değerine dönüşü modelde ayrı bir yerel değişikliktir; çerçevenin zamanlaması ölçülmez.
- Bu dersin tetiği: yerel.
Sonraki Adım
Bir sürüklemenin sonunda konum değişir ve görünüm yeni yerine geçer; bu geçişin ani mi yoksa yumuşak mı olacağı ayrı bir sorudur. Sonraki derste animasyonla sarılan değişikliğin, sarılmamış aynı değişiklikten tetik açısından ayrılıp ayrılmadığını ölçeceğiz.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.