Ders 09 / 15
Tablo Görünümleri
Yeniden kullanılan bir hücrede önceki satırın ekinin kalmasının artık, modeli değişen satırın yeniden yüklenmemesinin bayat sayıldığının ve hangi hücrenin geri geldiğinin ölçülmediğinin ölçümü.
Yayın Güncelleme
Bu derste
- Tablo görünümünün hücreyi yeniden kullanım tanımlayıcısıyla geri verdiğini ayırır
- Yeniden kullanılan hücrede önceki satırdan kalan değeri artık olarak ölçer
- Modeli değişen satırın yeniden yüklenmediği durumu bayat olarak ölçer
- Hangi hücrenin geri geldiğinin sonucu değiştirmediğini, ölçülen şeyin son yazılan değerler olduğunu doğrular
İçindekiler
Önceki derste, Yığıt Görünümleri, ekranda bir şeyi kaldırmanın onu gizlemek olduğunu ve gizlemeyi kodun yazması gerektiğini ölçtük. Tablo görünümü (table view) aynı yükün başka bir biçimini taşır. Uzun bir listede her satır için ayrı bir görünüm üretilmez; ekrandan çıkan hücre (cell) başka bir satır için yeniden kullanılır. Hücre temizlenmeden geri verilirse, bir önceki satırdan kalan her şey yeni satırın üzerinde durur.
Ö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. Bir hücre burada bir Screen’dir ve satırdan satıra aynı Screen nesnesine yeniden yazılır; model, kodun ekrana ne yazdığını kurar, çerçevenin kendisi değildir.
Yeniden Kullanılan Hücre
Bir kişi listesi düşünelim. Her satır bir ad gösterir; favori kişilerin yanında ayrıca bir not görünür. Tablo görünümü hücreyi yeniden kullanım tanımlayıcısıyla (reuse identifier) ister. Hücre yeni üretilmiş de olabilir, daha önce kullanılmış da.
import UIKit
final class ContactCell: UITableViewCell {
func configure(name: String, note: String?) {
textLabel?.text = name
if let note {
detailTextLabel?.text = note
}
}
}
func cellForRow(_ tableView: UITableView, at indexPath: IndexPath, contact: Contact) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell", for: indexPath) as! ContactCell
cell.configure(name: contact.name, note: contact.note)
return cell
}
configure(name:note:) notu yalnız varsa yazar; yoksa alanı olduğu gibi bırakır. Bu yüzden notsuz bir satır, notlu bir satırın hücresini devralırsa eski not ekranda kalır. Ölçümde üç adım vardır: ilk satır yazılır, hücre ikinci satır için yeniden kullanılır, sonra hücre temizlenir.
// main.swift — yeniden kullanilan hucrede onceki satirin notu kalirsa artik, temizlenirse olcum-disi
let cell = Screen("ContactCell")
let rowOne = ["name": "Ayse", "note": "Favori"]
let rowTwo = ["name": "Mehmet"]
cell.set("name", "Ayse")
cell.set("note", "Favori")
let first = check("satir-bir", model: rowOne, screen: cell, keys: ["name", "note"])
cell.set("name", "Mehmet")
let reused = check("yeniden-kullanim", model: rowTwo, screen: cell, keys: ["name", "note"])
cell.clear("note")
let cleaned = check("hazirlik-temizledi", model: rowTwo, screen: cell, keys: ["name", "note"])
countVerdicts(first + reused + cleaned)
satir-bir | ContactCell.name: model=Ayse ekran=Ayse -> uyumlu satir-bir | ContactCell.note: model=Favori ekran=Favori -> uyumlu yeniden-kullanim | ContactCell.name: model=Mehmet ekran=Mehmet -> uyumlu yeniden-kullanim | ContactCell.note: model=- ekran=Favori -> artik hazirlik-temizledi | ContactCell.name: model=Mehmet ekran=Mehmet -> uyumlu hazirlik-temizledi | ContactCell.note: model=- ekran=- -> olcum-disi uyumlu: 4 bayat: 0 eksik: 0 artik: 1
satir-bir adımında iki alan uyumlu. yeniden-kullanim adımında name uyumlu, çünkü ad yeniden yazıldı; note için model -, ekran Favori ve sonuç artik. Yeni satırın modelinde not yok, ama hücre önceki satırın notunu taşıyor. hazirlik-temizledi adımında alan temizlenince satır olcum-disi basar ve sayılmaz.
Sayım altı satırdan beşini kapsar: dört uyumlu, bir artık. Hücrenin yeniden kullanıldığı adım, artığın doğduğu tek yerdir.
DZ1. Bu ölçüm iki satırlık bir sıra için. Hücrenin her yeniden kullanımda temizlenmesi gerektiği gibi bir genel kural iddia edilmedi; ölçülen, kodun ekrana ne bıraktığıdır.
KAYNAK tarafta temizliği hücrenin kendisi yapabilir. Tablo görünümü, hücreyi kuyruktan geri vermeden hemen önce onun prepareForReuse() yöntemini çağırır; bu yöntemi yeniden tanımlayarak içerikle ilgisiz durum sıfırlanır, içerik ise cellForRowAt içinde her seferinde yeniden yazılır.
import UIKit
extension ContactCell {
override func prepareForReuse() {
super.prepareForReuse()
detailTextLabel?.text = nil
}
}
Önceki ölçümün hazirlik-temizledi adımı bu temizliğin sonucunu temsil eder: not alanı boşalır ve model de not taşımadığı için artık ortadan kalkar.
Model Değişir, Satır Yenilenmez
Hücrenin yanlış kalması tek sebep değildir. Bazen hücre doğru yapılandırılmıştır ve model sonradan değişir. Tablo görünümü, modelin değiştiğini kendiliğinden bilmez; satırın yeniden yüklenmesi çağrılmalıdır.
import UIKit
func contactRenamed(_ tableView: UITableView, row: Int) {
tableView.reloadRows(at: [IndexPath(row: row, section: 0)], with: .automatic)
}
Burada reloadRows(at:with:) çağrılırsa tablo ilgili satırın hücresini yeniden ister ve configure bir kez daha koşar. Çağrılmazsa hücre eski adı göstermeye devam eder. Ölçüm bu iki durumu ayrı adımlar olarak kurar.
// main.swift — model satir adini degistirir, satir yeniden yuklenmedikce bayat kalir
let cell = Screen("ContactCell")
var model = ["name": "Mehmet"]
cell.set("name", "Mehmet")
let before = check("satir-gosteriliyor", model: model, screen: cell, keys: ["name"])
model["name"] = "Mehmet Ali"
let stale = check("model-degisti", model: model, screen: cell, keys: ["name"])
cell.set("name", "Mehmet Ali")
let reloaded = check("satir-yenilendi", model: model, screen: cell, keys: ["name"])
countVerdicts(before + stale + reloaded)
satir-gosteriliyor | ContactCell.name: model=Mehmet ekran=Mehmet -> uyumlu model-degisti | ContactCell.name: model=Mehmet Ali ekran=Mehmet -> bayat satir-yenilendi | ContactCell.name: model=Mehmet Ali ekran=Mehmet Ali -> uyumlu uyumlu: 2 bayat: 1 eksik: 0 artik: 0
Birinci adımda uyumlu, ikinci adımda model Mehmet Ali, ekran Mehmet ve sonuç bayat. Üçüncü adımda satır yeniden yüklendi ve uyumlu. Sayım iki uyumlu, bir bayat. Yeniden kullanım artık üretti, yeniden yükleme atlandığında ise bayat çıktı: aynı listede iki farklı kopma, iki farklı kategori.
DZ2. Bu ölçüm tek bir hücreyi ve tek bir alanı kapsar. Aynı anda birden çok satırın değişmesi ölçülmedi.
Hücre Kaydı ve Yeniden Kullanım
Tablo görünümü yüzlerce satır gösterebilir, ama aynı anda yalnız ekrana sığan kadar hücre kurar. Ekrandan çıkan hücre atılmaz; bir havuza girer ve yeni satır için tekrar kullanılır. Bu yüzden hücre iki adımla elde edilir. Önce hücre sınıfı yeniden kullanım tanımlayıcısıyla (reuse identifier) tabloya kaydedilir; sonra veri kaynağı her satırda bu tanımlayıcıyla havuzdan bir hücre ister.
import UIKit
final class ContactListController: UITableViewController {
var contacts: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "ContactCell")
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
contacts.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "ContactCell", for: indexPath)
cell.textLabel?.text = contacts[indexPath.row]
return cell
}
}
İki zorunlu veri kaynağı yordamı vardır: kaç satır olduğunu söyleyen yordam ve her satırın hücresini kuran yordam. İkincisi her seferinde çağrıldığı için hücrenin bütün alanlarını yazmak zorundadır. Havuzdan gelen hücre önceki satırdan kalan değerleri taşır; bir alan yazılmazsa eski değer görünür.
Hücrenin Hazırlanması
Havuza dönen hücre kendini temizleme fırsatı bulur. Hücre sınıfı prepareForReuse yordamını yeniden tanımlayarak içerikle ilgisiz durumu (seçim, saydamlık, düzenleme kipi) sıfırlayabilir. İçeriğin kendisi ise her zaman cellForRowAt içinde yeniden yazılır; satırın artığını silmenin güvenli yeri orasıdır.
import UIKit
final class NoteCell: UITableViewCell {
let noteLabel = UILabel()
override func prepareForReuse() {
super.prepareForReuse()
noteLabel.text = nil
accessoryType = .none
}
}
Burada iki alan boşaltılmıştır: not metni ve ek simge. Bir alan atlanırsa onun değeri bir sonraki satırda belirir. Hücre temizlenmesi ile veri kaynağındaki atama birbirini tamamlar; ikisinden yalnız birine güvenmek yetersizdir.
Veri Değişince Satır Yenilenmez
Tablo görünümü, veri kaynağının içeriğini kendiliğinden izlemez. Dizi değiştiğinde ekran aynı kalır, ta ki tabloya haber verilene kadar. Haber vermenin iki yolu vardır: reloadData bütün tabloyu yeniden yükler, reloadRows(at:with:) ise yalnız belirtilen satırları. Tek bir satırın adı değiştiyse ikincisi daha ucuzdur ve kaydırma konumunu korur. Satır eklemek ya da silmek ise insertRows ve deleteRows ile yapılır; bunları veri dizisini aynı anda değiştirmeden çağırmak çalışma anında bir tutarsızlık hatasına yol açar.
Tablo Görünümü mü Koleksiyon Görünümü mü
Tablo görünümü tek sütunlu, düşey kayan listeler için tasarlanmıştır ve başlık, alt bilgi, düzenleme kipi ve kaydırarak silme gibi liste davranışlarını hazır getirir. Bir ayar uygulamasındaki satırlar, kişi listesi ya da sipariş geçmişi bunun tipik örnekleridir. Öğeler birden çok sütuna yayılıyorsa, yatay kayıyorsa ya da satır biçimi öğeden öğeye değişiyorsa koleksiyon görünümü daha uygundur. Koleksiyon görünümü liste biçimini de destekler, bu yüzden yeni kurulan ekranlarda ikisi arasındaki fark giderek azalmıştır; ama tablo görünümü hâlâ basit listeler için daha az kod ister.
Yeniden kullanım kavramı ikisinde de aynıdır. Hücre havuzdan gelir, bu yüzden hücreyi yapılandıran kodun her alanı yazması gerekir. Bu dersin artık satırları, hangi görünümü seçtiğinizden bağımsız olarak karşınıza çıkacaktır.
Sık Yapılan Hatalar
- Hücreyi kaydetmeden havuzdan istemek. Kayıtsız tanımlayıcıyla yapılan istek çalışma anında uygulamayı durdurur.
- Hücrenin her alanını yazmamak. Havuzdan gelen hücre eski satırın değerlerini taşır; bu dersin ilk ölçümündeki artık satırı budur.
- Hücrede kalıcı durum tutmak. Hücre satırın değil, o anki ekranın parçasıdır; seçili olma gibi durum veri kaynağında saklanmalıdır.
- Hücrenin içinde ağır iş yapmak. Kaydırma sırasında her yeni satır
cellForRowAtçağırır ve uzun süren iş akıcılığı bozar. - Veri dizisini değiştirip tabloya haber vermemek. Satırın adı değişir, ama ekran eski adı göstermeye devam eder; ikinci ölçümün bayat sonucu budur.
Ölçülen Sonucun Yorumu
Üç ölçümün ortak noktası, hücrenin ve verinin ayrı yaşamasıdır. Birinci ölçümde hücre önceki satırın notunu taşıdı ve model o satırda not tutmadığı için sonuç artık oldu. Hücre temizlenince satır ölçüm dışı kaldı, çünkü iki tarafta da değer yoktu. İkinci ölçümde model değişti ama tabloya haber verilmedi, ekran eski kaldı ve sonuç bayat oldu. Üçüncü ölçümde ise hangi hücrenin geri geldiği ölçülmedi: aynı kalıntı aynı sonucu verir. Bu, yeniden kullanımın sessiz bir davranış olduğunu gösterir. Hata yoktur, uyarı yoktur; yalnız eski bir değer vardır ve onu bulmanın tek yolu modelle karşılaştırmaktır.
Burada pratik bir kural çıkarılabilir: hücreyi yapılandıran kod, bir alanı yazmıyorsa o alanı boşaltmak zorundadır. Alanı atlayan her satır, havuzdan gelen hücrenin önceki değerini yeniden gösterme riski taşır ve bu risk yalnız kaydırma sırasında, belirli bir satır sırasında ortaya çıktığı için testte kaçabilir.
Sınırlayıcı Ölçüm
Yeniden kullanımın bir de sayısal yüzü vardır: tablo görünümü kaç hücre tutar, hangi hücre hangi satıra geri döner. Bu dersin ölçüsü bunları kapsamaz. Kuyruk boyutu ve hangi hücrenin geri geldiği ölçülmez. Model yalnız bir hücrenin iki yapılandırmasını sırayla kurar ve son yazılan değerlere bakar.
Bunu göstermek için aynı kalıntıyı iki farklı hücrede kuralım: biri aynı hücre, biri başka bir hücre olsun. Kural için ikisi arasında fark yoktur.
// main.swift — hangi hucrenin geri geldigi olculmez, ayni kalinti ayni sonucu verir
let model = ["name": "Mehmet"]
let sameCell = Screen("SameCell")
let otherCell = Screen("OtherCell")
sameCell.set("name", "Mehmet")
sameCell.set("note", "Favori")
otherCell.set("name", "Mehmet")
otherCell.set("note", "Favori")
let sameResult = check("ayni-hucre", model: model, screen: sameCell, keys: ["name", "note"])
let otherResult = check("baska-hucre", model: model, screen: otherCell, keys: ["name", "note"])
countVerdicts(sameResult + otherResult)
ayni-hucre | SameCell.name: model=Mehmet ekran=Mehmet -> uyumlu ayni-hucre | SameCell.note: model=- ekran=Favori -> artik baska-hucre | OtherCell.name: model=Mehmet ekran=Mehmet -> uyumlu baska-hucre | OtherCell.note: model=- ekran=Favori -> artik uyumlu: 2 bayat: 0 eksik: 0 artik: 2
İki hücre için sonuç birebir aynı: ad uyumlu, not artık. Sayım iki uyumlu, iki artık. Kalıntının hangi hücreden geldiği çıktıya girmedi, çünkü modelde hücrenin kimliğini taşıyan bir alan yok. Bu bir sınırdır: model, ekranın son durumunu karşılaştırır, o duruma hangi hücrenin yol açtığını değil.
DZ3. Bu ölçüm iki ekran nesnesinin aynı değerleri taşımasını sınar. Tablo görünümünün kaç hücreyi kuyrukta tuttuğuna dair bir sayı yazılmadı.
Özet
- Tablo görünümü hücreyi yeniden kullanım tanımlayıcısıyla geri verir; geri gelen hücre önceki satırın değerlerini taşıyabilir.
- Yeni satırın modelinde olmayan ama hücrede kalan değer artıktır; hücre hazırlanırken temizlenirse ölçüm dışına düşer.
- Modeli değişen satır yeniden yüklenmezse ekran bayat kalır.
- Yeniden kullanım artık, atlanan yeniden yükleme bayat üretir; iki ayrı kopma iki ayrı kategoridedir.
- Kuyruk boyutu ve hangi hücrenin geri geldiği ölçülmez; model son yazılan değerleri karşılaştırır.
- Bu dersin uyumu: artık.
Sonraki Adım
Tablo görünümü tek sütunlu bir listedir. Sıradaki derste koleksiyon görünümü öğeleri iki boyutta dizer ve veriye öğe eklendiğinde ekranın bunu öğrenmesi gerekir. O derste yeni bir uyum görülecek: eksik.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.