İçeriğe geç
academia.sh

Ders 10 / 15

Koleksiyon Görünümleri

Veri kaynağına eklenen bir öğenin koleksiyon görünümüne bildirilmediği durumun eksik, anlık görüntü uygulandığında uyumlu sayıldığının ve konum hesabının ölçülmediğinin ölçümü.

Yayın Güncelleme

Bu derste

  • Koleksiyon görünümünün öğeleri veri kaynağından aldığını ve eklemenin ayrıca bildirildiğini ayırır
  • Veri kaynağına eklenen ama ekrana yazılmayan öğeyi eksik olarak ölçer
  • Anlık görüntü uygulandığında eklenen ve kaldırılan öğelerin birlikte uyuma geldiğini gösterir
  • Esnek düzenin konum hesabının ölçülmediğini, modelde konum anahtarı olmadığını doğrular
İçindekiler

Önceki derste, Tablo Görünümleri, bir hücrenin yeniden kullanıldığında önceki satırın izini taşıyabildiğini ve model değiştiğinde satırın ayrıca yeniden yüklenmesi gerektiğini ölçtük. Koleksiyon görünümü (collection view) aynı fikri iki boyuta taşır: öğeler bir ızgarada ya da başka bir esnek düzende dizilir. Bu dersin ölçüsü öğelerin sayısına bakar. Veri kaynağına bir öğe eklendiğinde ekranın onu görüp görmediği, ayrı bir bildirimle belirlenir.

Ölçüm çekirdek dosyayla yapılı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)")
    }
}

Çekirdek, ekranda gösterilen her alanı modelin o anki değeriyle karşılaştırır. Bu derste her öğe bir anahtardır: öğenin kimliği anahtar, başlığı değerdir. Model, kodun ekrana ne yazdığını kurar; çerçevenin kendisi değildir.

Eklenen Öğe Ekrana Ulaşmayabilir

Bir görev panosu düşünelim. Pano, görevleri bir koleksiyon görünümünde gösterir. Görev dizisi veri kaynağında (data source) durur; koleksiyon görünümü öğe sayısını ve hücreleri oradan ister. Diziye bir görev eklendiğinde görünüm bunu kendiliğinden bilmez.

import UIKit

final class BoardController: UIViewController, UICollectionViewDataSource {
    var tasks = ["Rapor", "Test"]
    @IBOutlet var collectionView: UICollectionView!

    func addTask(_ title: String) {
        tasks.append(title)
        collectionView.insertItems(at: [IndexPath(item: tasks.count - 1, section: 0)])
    }
}

addTask(_:) diziye ekler ve ardından insertItems(at:) çağırır. Bu çağrı olmazsa koleksiyon görünümü eski öğe sayısıyla çalışmaya devam eder. Ölçümde üç adım vardır: pano açılır, model üçüncü görevi alır, ekran bunu yazar.

// main.swift — veri kaynagina eklenen oge bildirilmedikce eksik kalir
let board = Screen("TaskBoard")
var model = ["task1": "Rapor", "task2": "Test"]

board.set("task1", "Rapor")
board.set("task2", "Test")
let opened = check("acilis", model: model, screen: board, keys: ["task1", "task2", "task3"])

model["task3"] = "Yayin"
let added = check("oge-eklendi", model: model, screen: board, keys: ["task1", "task2", "task3"])

board.set("task3", "Yayin")
let notified = check("ekleme-bildirildi", model: model, screen: board, keys: ["task1", "task2", "task3"])

countVerdicts(opened + added + notified)
acilis | TaskBoard.task1: model=Rapor ekran=Rapor -> uyumlu
acilis | TaskBoard.task2: model=Test ekran=Test -> uyumlu
acilis | TaskBoard.task3: model=- ekran=- -> olcum-disi
oge-eklendi | TaskBoard.task1: model=Rapor ekran=Rapor -> uyumlu
oge-eklendi | TaskBoard.task2: model=Test ekran=Test -> uyumlu
oge-eklendi | TaskBoard.task3: model=Yayin ekran=- -> eksik
ekleme-bildirildi | TaskBoard.task1: model=Rapor ekran=Rapor -> uyumlu
ekleme-bildirildi | TaskBoard.task2: model=Test ekran=Test -> uyumlu
ekleme-bildirildi | TaskBoard.task3: model=Yayin ekran=Yayin -> uyumlu
uyumlu: 7
bayat: 0
eksik: 1
artik: 0

acilis adımında task3 her iki tarafta da yok ve olcum-disi basar; öteki ikisi uyumlu. oge-eklendi adımında task3 için model Yayin, ekran - ve sonuç eksik: veri kaynağı öğeyi biliyor, ekran bilmiyor. ekleme-bildirildi adımında öğe yazılınca üç satır da uyumlu.

Sayım dokuz satırın sekizini kapsar: yedi uyumlu, bir eksik. Eksik burada bir öğe düzeyinde doğuyor: modelde olan, ekranda hiç olmayan bir görev.

DZ1. Bu ölçüm üç görevle yapıldı ve eklenen öğe sonda duruyor. Öğenin dizinin ortasına eklenmesi ölçülmedi.

Anlık Görüntü Her Şeyi Birlikte Yazar

Tek tek insertItems(at:) çağırmak, silme ve taşıma da eklendiğinde dizinleri elle eşlemeyi gerektirir. Bir anlık görüntü (snapshot) bu yükü kaldırır: veri kaynağına tüm öğelerin yeni listesi verilir, görünüm farkı kendisi bulur.

import UIKit

func applyBoard(_ tasks: [String], to dataSource: UICollectionViewDiffableDataSource<Int, String>) {
    var snapshot = NSDiffableDataSourceSnapshot<Int, String>()
    snapshot.appendSections([0])
    snapshot.appendItems(tasks)
    dataSource.apply(snapshot)
}

Burada ekleme ve silme tek çağrıyla ekrana gider. Ölçümde model aynı anda bir görevi eklesin, bir görevi kaldırsın. Anlık görüntü yazılana kadar ikisi de ekrana ulaşmamıştır. Yardımcı işlev, ekrandaki öğelerin tümünü modele göre yeniden yazar.

// main.swift — anlik goruntu eklemeyi ve kaldirmayi birlikte uygular
func applySnapshot(_ items: [String: String], to screen: Screen) {
    for key in screen.shown.keys.sorted() where items[key] == nil {
        screen.clear(key)
    }
    for (key, value) in items.sorted(by: { $0.key < $1.key }) {
        screen.set(key, value)
    }
}

let board = Screen("TaskBoard")
let model = ["task2": "Test", "task3": "Yayin"]

board.set("task1", "Rapor")
board.set("task2", "Test")
let before = check("anlik-goruntu-oncesi", model: model, screen: board, keys: ["task1", "task2", "task3"])

applySnapshot(model, to: board)
let after = check("anlik-goruntu-sonrasi", model: model, screen: board, keys: ["task1", "task2", "task3"])

countVerdicts(before + after)
anlik-goruntu-oncesi | TaskBoard.task1: model=- ekran=Rapor -> artik
anlik-goruntu-oncesi | TaskBoard.task2: model=Test ekran=Test -> uyumlu
anlik-goruntu-oncesi | TaskBoard.task3: model=Yayin ekran=- -> eksik
anlik-goruntu-sonrasi | TaskBoard.task1: model=- ekran=- -> olcum-disi
anlik-goruntu-sonrasi | TaskBoard.task2: model=Test ekran=Test -> uyumlu
anlik-goruntu-sonrasi | TaskBoard.task3: model=Yayin ekran=Yayin -> uyumlu
uyumlu: 3
bayat: 0
eksik: 1
artik: 1

Öncesinde task1 için model - ve ekran Rapor, sonuç artik; task3 için model Yayin, ekran -, sonuç eksik; task2 uyumlu. Sonrasında task1 iki tarafta da yok, olcum-disi; task3 ve task2 uyumlu. Sayım üç uyumlu, bir eksik, bir artık. Aynı anlık görüntü iki farklı kopmayı birlikte kapattı.

DZ2. Bu ölçüm applySnapshot yardımcısını modelin ekran tarafı sayar; gerçek apply çağrısının animasyonlu mu animasyonsuz mu koştuğu ölçülmedi.

Veri Kaynağı ve Anlık Görüntü

Koleksiyon görünümü tablo görünümünden daha esnektir: satır yerine öğe (item) gösterir ve öğeleri ızgara, sütun ya da yazarın tasarladığı herhangi bir düzende yerleştirebilir. Verinin ekrana nasıl aktarılacağı ise iki yoldan biriyle yapılır. Birincisi tablodaki gibi sayım ve hücre yordamlarını yazmaktır; ikincisi farklılık veren veri kaynağı (diffable data source) kurmaktır.

Farklılık veren veri kaynağı, ekranda ne olacağını bir anlık görüntü (snapshot) ile söyler. Anlık görüntü bölümleri ve her bölümdeki öğe kimliklerini taşıyan bir değerdir. Yeni bir anlık görüntü uygulandığında veri kaynağı eskisiyle yenisi arasındaki farkı hesaplar ve yalnız değişen öğeleri ekler, siler ya da taşır. Böylece reloadData çağırmak ya da satır dizinlerini elle hesaplamak gerekmez.

import UIKit

enum BoardSection {
    case main
}

final class TaskBoardController: UIViewController {
    var dataSource: UICollectionViewDiffableDataSource<BoardSection, String>!

    func apply(titles: [String]) {
        var snapshot = NSDiffableDataSourceSnapshot<BoardSection, String>()
        snapshot.appendSections([.main])
        snapshot.appendItems(titles)
        dataSource.apply(snapshot, animatingDifferences: true)
    }
}

İki tür parametre vardır: bölüm kimliği ve öğe kimliği. İkisi de Hashable olmalıdır. Kimlik, öğenin değerini değil kimliğini taşır; bu yüzden iki öğenin aynı kimliği paylaşması çalışma anında bir hataya yol açar. Kimlik benzersiz değilse anlık görüntü kurulamaz.

Hücre Kaydı

Koleksiyon görünümünde hücre kaydı bir kayıt nesnesiyle yapılır. Kayıt nesnesi hücrenin nasıl yapılandırılacağını bir kapanışla bildirir ve veri kaynağı bu nesneyi hücre sağlayıcısında kullanır. Böylece tanımlayıcı dizeler kaybolur; yanlış yazılmış bir tanımlayıcının çalışma anında patlaması sorunu da ortadan kalkar.

import UIKit

func makeDataSource(for collectionView: UICollectionView) -> UICollectionViewDiffableDataSource<BoardSection, String> {
    let registration = UICollectionView.CellRegistration<UICollectionViewListCell, String> { cell, indexPath, title in
        var content = cell.defaultContentConfiguration()
        content.text = title
        cell.contentConfiguration = content
    }
    return UICollectionViewDiffableDataSource(collectionView: collectionView) { collectionView, indexPath, title in
        collectionView.dequeueConfiguredReusableCell(using: registration, for: indexPath, item: title)
    }
}

Hücre sağlayıcısı her görünür öğe için çağrılır ve öğe kimliğini alır. Kimlikten içeriğe giden yol kapanışın içindedir; burada kimlik doğrudan başlık olarak kullanılmıştır, ama gerçek uygulamada kimlik bir modelin tanımlayıcısıdır ve içerik ayrı bir kaynaktan okunur.

Düzen Ayrı Bir Nesnedir

Koleksiyon görünümünün öğeleri nasıl yerleştirdiği düzen (layout) nesnesinde saklıdır. Veri kaynağı ve düzen birbirinden bağımsızdır: aynı anlık görüntü hem ızgara hem liste düzeniyle gösterilebilir. Bu dersin üçüncü ölçümü bunu doğrular; ızgara ve liste panoları aynı kimlikleri taşır ve düzen farkı çıktıya girmez.

Düzen Seçimi

Koleksiyon görünümünün düzeni, görünümün ne kadar esnek olacağını belirler. Akış düzeni öğeleri satır satır dizer ve sabit boyutlu ızgaralar için yeterlidir. Birleşik düzen ise bölümlere, gruplara ve öğelere ayrı boyut tarifi verir; karışık biçimli ekranlar, yatay kayan şeritler ve başlıklı bölümler bu düzenle kurulur. Liste düzeni tablo görünümünün davranışlarını koleksiyon görünümüne getirir. Düzen seçimi veri kaynağını etkilemez; aynı anlık görüntü her üç düzenle de gösterilebilir.

Bu ayrım, anlık görüntü yaklaşımının asıl kazancıdır. Veri bir yerde, görünüm biçimi başka bir yerde kalır ve ikisi birbirini bilmez. Ekranın düzenini değiştirmek için veriye dokunmak gerekmez; veriyi değiştirmek için de düzene.

Sık Yapılan Hatalar

  • Kimliğin değer olarak kullanılması. Öğenin görünen metni değişebilir; kimlik değişmemelidir. Kimlik olarak metni kullanan veri kaynağı, metin değişince öğeyi silinip yeniden eklenmiş sayar.
  • Aynı kimliği iki kez eklemek. Anlık görüntü yinelenen kimliği kabul etmez.
  • Anlık görüntüyü her seferinde sıfırdan kurup öğeleri yeniden eklemeyi unutmak. Eksik öğe ekrandan kalkar.
  • Mevcut öğenin içeriği değişince anlık görüntüyü uygulayıp farkın çıkmasını beklemek. Kimlik aynı kaldığında veri kaynağı farkı görmez; içeriği yenilemek için ayrıca yeniden yükleme istenir.
  • Düzen ile veri kaynağını aynı yerde karıştırmak. Düzen bilgisini hücre sağlayıcısına yazmak bakımı zorlaştırır.

Ölçülen Sonucun Yorumu

İlk ölçümde üçüncü görev modele girdi ama veri kaynağına bildirilmedi: oge-eklendi adımında model Yayin iken ekran boştu ve satır eksik çıktı. Ekleme bildirilince aynı satır uyumlu oldu. İkinci ölçümde iki fark birlikte vardı: birinci görev modelden kalkmıştı ama ekranda duruyordu, bu artık satırdı; üçüncü görev ise eksikti. Anlık görüntü uygulandıktan sonra birinci görev iki tarafta da yoktu ve ölçüm dışı kaldı, üçüncü görev uyumlu oldu. Sayım üç uyumlu, bir eksik ve bir artık verdi; sayıma yalnız ilk adımdaki durum girdi. Üçüncü ölçüm düzenin uyumu etkilemediğini gösterdi: ızgara ve liste aynı dört satırı uyumlu verdi, sütun sayısı çıktıya girmedi.

Üç sonuç aynı noktaya çıkar. Farklılık veren veri kaynağı farkı hesaplar, ama farkın girdisi olan anlık görüntüyü her değişiklikte vermek geliştiricinin işidir. Anlık görüntü uygulanmadığı sürece model ile ekran ayrı yaşar: eklenen öğe eksik, silinen öğe artık olarak kalır.

Pratik kural şudur: model ne zaman değişirse, yeni bir anlık görüntü kurulur ve uygulanır. Anlık görüntüyü model değişikliğinin kendisine bağlamak, unutulma ihtimalini azaltır; ayrı bir yerde güncellemek ise ekranı bayat bırakan değişikliklerin başlıca kaynağıdır.

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

Koleksiyon görünümünün özü esnek düzendir: öğeler üç sütunda da dizilebilir, tek sütunda da. Hangi öğenin hangi satıra ve sütuna düştüğü düzenin hesabıdır. Esnek düzenin konum hesabı ölçülmez; modelde bir konum anahtarı yoktur, anahtar yalnız öğenin kimliğidir.

İki pano aynı öğeleri taşısın: biri üç sütunlu bir ızgara, öteki tek sütunlu bir liste olarak düşünülsün. Sütun sayısı modelde bir alan değildir, bu yüzden ikisi aynı sonucu verir.

// main.swift — sutun sayisi modelde yok, iki duzen ayni sonucu verir
let model = ["task1": "Rapor", "task2": "Test"]
let grid = Screen("GridBoard")
let list = Screen("ListBoard")

for (key, value) in model.sorted(by: { $0.key < $1.key }) {
    grid.set(key, value)
    list.set(key, value)
}

let gridResult = check("izgara", model: model, screen: grid, keys: ["task1", "task2"])
let listResult = check("liste", model: model, screen: list, keys: ["task1", "task2"])

countVerdicts(gridResult + listResult)
izgara | GridBoard.task1: model=Rapor ekran=Rapor -> uyumlu
izgara | GridBoard.task2: model=Test ekran=Test -> uyumlu
liste | ListBoard.task1: model=Rapor ekran=Rapor -> uyumlu
liste | ListBoard.task2: model=Test ekran=Test -> uyumlu
uyumlu: 4
bayat: 0
eksik: 0
artik: 0

Dört satırın hepsi uyumlu ve sayım dört uyumlu. Izgara ile liste arasındaki fark çıktıya girmedi. Bu bir sınırdır: model öğenin ekranda olup olmadığını söyler, nerede durduğunu söylemez.

DZ3. Bu ölçüm iki panonun aynı anahtar kümesini taşımasını sınar. Öğelerin sütunlara dağılımına dair bir sayı yazılmadı.

Özet

  • Koleksiyon görünümü öğeleri veri kaynağından alır; eklemenin ayrıca bildirilmesi gerekir.
  • Veri kaynağına eklenen ama ekrana yazılmayan öğe eksiktir.
  • Anlık görüntü eklemeyi ve kaldırmayı birlikte uygular; önceki artık ve eksik tek adımda kapanır.
  • Hiç var olmamış ve iki tarafta da olmayan öğe ölçüm dışıdır.
  • Esnek düzenin konum hesabı ölçülmez; model yalnız öğenin ekranda olup olmadığını karşılaştırır.
  • Bu dersin uyumu: eksik.

Sonraki Adım

Tablo ve koleksiyon görünümleri veriyi ve davranışı kendileri tutmaz, başka bir nesneye danışır. Sıradaki derste bu danışma ilişkisi, temsilci kalıbı, ele alınacak. Zayıf tutulan bir bağ boşa düştüğünde ekran bayat kalabilir.

İ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