Ders 04 / 15
Sunum Biçimleri
Bir ekranın üzerine kip sunumla açılan not ekranında girilen değerin modele yazıldığı hâlde alttaki ekrana ulaşmadığının ve ekranda eksik kaldığının ölçümü.
Yayın Güncelleme
Bu derste
- İtmeli sunum ile kip sunum arasındaki farkı ekranın nereye konduğu üzerinden ayırır
- Kip sunumda girilen değerin modele yazılıp alttaki ekrana yazılmadığı durumu ölçer
- Alttaki ekranın mevcut alanlarının sunum sırasında uyumlu kaldığını gösterir
- Ölçümün yalnız görünür ekrana baktığını ve kapatılan ekranı saymadığını ayırır
İçindekiler
Önceki derste ekranlar bir yığıtta üst üste konuyordu. Bir ekranı göstermenin bir yolu daha vardır: mevcut ekranın üzerine kip sunum (modal presentation) olarak açmak. Kip sunum yığıta eklemez; sunulan ekran, sunan ekranın üzerinde durur ve kapatılınca yerini boş bırakır. Yığıta ekleme ise itmeli sunum diye ayrılır.
Bu ayrım ölçüm açısından önemlidir, çünkü kip sunumda iki ekran arasında hazır bir bağ yoktur. Sunulan ekranın girdiği değer, sunan ekrana kendiliğinden ulaşmaz. Bu derste girişin modele yazıldığı ama sunan ekranın onu hiç göstermediği bir durumu ölçeriz.
Sunmak ve Kapatmak
Aşağıdaki parça bir görev panosunu ve üzerine açılan bir not ekranını gösterir. Platform çerçevesini içe aldığı için çalıştırılmaz.
import UIKit
final class TaskBoardController: UIViewController {
let store: TaskStore
private let titleLabel = UILabel()
private let openLabel = UILabel()
init(store: TaskStore) {
self.store = store
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewDidLoad() {
super.viewDidLoad()
titleLabel.text = store.values["title"] ?? ""
openLabel.text = store.values["open"] ?? ""
}
func addNote() {
let controller = NoteController(store: store)
controller.modalPresentationStyle = .pageSheet
present(controller, animated: true)
}
}
final class NoteController: UIViewController {
let store: TaskStore
private let titleLabel = UILabel()
private let noteLabel = UILabel()
init(store: TaskStore) {
self.store = store
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
override func viewDidLoad() {
super.viewDidLoad()
titleLabel.text = store.values["title"] ?? ""
}
func save(_ text: String) {
store.update("note", text)
noteLabel.text = text
dismiss(animated: true)
}
}
addNote sunum biçimini seçer ve not ekranını üste açar; save hem modele hem kendi etiketine yazar ve kendini kapatır. Panonun denetleyicisinde note anahtarını okuyan hiçbir satır yoktur. Sunum biçimi, yani sayfa mı yoksa tam ekran mı olacağı, sunan ekranın kapatmadan sonra yeniden görünme yöntemlerinin çağrılıp çağrılmayacağını da etkileyebilir. Bu ders bunu ölçmez ve ona güvenmez.
Sunum Biçimleri
Gezinme yığıtına eklemenin yanında ikinci yol, bir denetleyiciyi başka bir denetleyicinin üstünde sunmaktır. Kip sunum, yeni ekranı mevcut ekranın üstüne açar ve kullanıcı kapatana kadar altındaki ekranla etkileşimi keser. Sunum biçimi modalPresentationStyle özelliğiyle seçilir: fullScreen ekranı baştan sona kaplar, pageSheet altındaki ekranı kısmen açıkta bırakan bir sayfa gösterir.
import UIKit
final class TaskBoardController: UIViewController {
let store: TaskStore
init(store: TaskStore) {
self.store = store
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
func addNote() {
let note = NoteController(store: store)
note.modalPresentationStyle = .pageSheet
present(note, animated: true)
}
}
final class NoteController: UIViewController {
let store: TaskStore
init(store: TaskStore) {
self.store = store
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) { fatalError("init(coder:) is not supported") }
func saveTapped() {
store.update("note", "Aranacak")
dismiss(animated: true)
}
}
Sunan denetleyici present çağırır, sunulan denetleyici kendini dismiss ile kapatır. Sunulan ekran kapandığında sunan ekran yalnızca görünür hale gelir; sunulan denetleyicinin ürettiği sonuç sunana kendiliğinden taşınmaz.
Ölçü
Çekirdek önceki derslerle aynıdı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)")
}
}
Not Kaydedilince Pano
Model iki denetleyiciyi saf Swift ile kurar. Model, kodun ekrana ne yazdığını kurar; çerçevenin kendisi değildir. present ve dismiss yalnız hangi ekranın üstte olduğunu kaydeder. Olaylar şunlardır: pano yüklenir, not ekranı sunulur ve yüklenir, kullanıcı bir not kaydeder ve not ekranı kapanır.
// main.swift — pano, kip sunum, not
final class TaskStore {
private(set) var values: [String: String] = [:]
func update(_ key: String, _ value: String) { values[key] = value }
func remove(_ key: String) { values[key] = nil }
}
final class TaskBoardController {
let store: TaskStore
let screen = Screen("TaskBoardController")
private(set) var presented: String?
init(store: TaskStore) { self.store = store }
func viewDidLoad() {
screen.set("title", store.values["title"] ?? "")
screen.set("open", store.values["open"] ?? "")
}
func present(_ name: String) { presented = name }
func dismiss() { presented = nil }
}
final class NoteController {
let store: TaskStore
let screen = Screen("NoteController")
init(store: TaskStore) { self.store = store }
func viewDidLoad() {
screen.set("title", store.values["title"] ?? "")
}
func save(_ text: String) {
store.update("note", text)
screen.set("note", text)
}
}
let store = TaskStore()
store.update("title", "Rapor")
store.update("open", "4")
let board = TaskBoardController(store: store)
board.viewDidLoad()
let loaded = check("yuklendi", model: store.values, screen: board.screen, keys: ["title", "open"])
let note = NoteController(store: store)
board.present("NoteController")
note.viewDidLoad()
let presented = check("not-sunuldu", model: store.values, screen: note.screen, keys: ["title"])
note.save("Ara")
board.dismiss()
let saved = check("not-kaydedildi", model: store.values, screen: note.screen, keys: ["note"])
let closed = check("not-kapandi", model: store.values, screen: board.screen, keys: ["title", "open", "note"])
countVerdicts(loaded + presented + saved + closed)
yuklendi | TaskBoardController.title: model=Rapor ekran=Rapor -> uyumlu yuklendi | TaskBoardController.open: model=4 ekran=4 -> uyumlu not-sunuldu | NoteController.title: model=Rapor ekran=Rapor -> uyumlu not-kaydedildi | NoteController.note: model=Ara ekran=Ara -> uyumlu not-kapandi | TaskBoardController.title: model=Rapor ekran=Rapor -> uyumlu not-kapandi | TaskBoardController.open: model=4 ekran=4 -> uyumlu not-kapandi | TaskBoardController.note: model=Ara ekran=- -> eksik uyumlu: 6 bayat: 0 eksik: 1 artik: 0
Not ekranı kendi alanında uyumlu: kullanıcı girdiğini ekranında görüyor ve model aynı değeri taşıyor. Pano ise title ve open alanlarında uyumlu kaldı, çünkü onları sunum boyunca kimse değiştirmedi. note alanı modele yeni girdi ve panonun ekranı onu hiç yazmadı; satır eksik çıktı. Sunum kapandığında pano kendiliğinden güncellenmez, ve hangi bilginin kimde olduğunu bilen kod yoktur.
Kip sunum, panoyu çalışır hâlde bırakır ama panoya haber vermez. Haberin ulaşması için sunan ekranın bir araya girmesi gerekir; bunun bilinen yolu bir temsilci protokolüdür. Bu protokol açılış dersinde klasör seçici için kuruldu ve burada aynı biçimde kullanılabilir. Bu derste ölçülen şey eksikliğin kendisidir.
GD1. Modelde save sunulan ekranın kendi etiketini yazar ve dismiss yalnız üstteki ekranı kaldırır. Gerçek çerçevede sunum biçimine göre kapatma sırasında sunan ekranın hangi yaşam döngüsü yöntemlerinin çağrıldığı ölçülmedi; panonun hiçbirinde note yazmadığı varsayılır, bu yüzden sonuç çağrılardan bağımsızdır.
Sık Yapılan Hatalar
- Sonucun sunana ulaştığını varsaymak. Sunulan ekran modeli değiştirir, ama sunan ekran, yeniden göründüğünde yazma çağrısı yoksa eski değeri gösterir. Bu dersin ölçtüğü eksik sonucu budur.
- Sunulan ekranın sunanı gizleyeceğini beklemek.
pageSheetgibi kısmi biçimlerde alttaki ekran yaşam döngüsü açısından kalkmış sayılmaz;viewWillAppeardönüşte çağrılmayabilir. Tam ekran biçimde ise çağrılır. Güncellemeyi bu yöntemlere bağlayan kod, biçim değişince sessizce çalışmaz hale gelir. - Kapatma düğmesine güvenmek. Sayfa biçiminde kullanıcı ekranı aşağı çekerek de kapatabilir. Kapatmaya bağlı bir kaydetme çağrısı yalnız düğmede duruyorsa bu yoldan atlanır; kapatma
isModalInPresentationile engellenebilir ya da çıkış bir temsilciyle ele alınır. - Bir sunum sürerken ikincisini başlatmak. Çerçeve bunu reddeder ve sunum yapılmaz.
- Sunulan denetleyiciyi sunan tutarken geri çağrı bağını güçlü kurmak. Temsilci zayıf tutulmazsa iki denetleyici birbirini serbest bırakmaz.
Ölçümü Okumak
Yedi satırın altısı uyumlu, biri eksiktir. Eksik satır, not kaydedilip ekran kapatıldıktan sonra panoda görünmeyen note alanıdır: model notu taşıyor, pano ekranı onu hiç yazmadı. Kayıt yapılmıştır; kaybolan, sonucun panoya taşınmasıdır.
Bu sonuçta uyumlu satırların bir anlamı vardır: not ekranının kendi alanları, sunulduğu sürece modelle uyumludur. Hata sunan ve sunulan ekran arasındaki sınırda ortaya çıkmıştır. Kip sunum, iki ekranı ayrı yaşam döngülerine koyar ve aralarındaki taşıma programcıya bırakılır. Çözüm iki ekranı da modelden okutmak, ya da sunulan ekran kapanınca sunanın alanları yeniden yazmasını sağlamaktır; hangisinin seçileceği ekranın yapısına bağlıdır.
Sonucu Sunana Taşımak
Sunulan ekranın sonucunu sunana ulaştırmanın olağan yolu bir temsilci protokolüdür. Sunulan denetleyici protokolü çağırır, sunan denetleyici onu uygular. Temsilci referansı zayıf tutulur, çünkü sunan denetleyici sunulanı zaten tutar ve ters yönde güçlü bir bağ bir tutma döngüsü kurardı.
import UIKit
protocol NoteControllerDelegate: AnyObject {
func noteController(_ controller: NoteController, didSave text: String)
}
final class NoteController: UIViewController {
weak var delegate: NoteControllerDelegate?
func saveTapped() {
delegate?.noteController(self, didSave: "Aranacak")
dismiss(animated: true)
}
}
extension TaskBoardController: NoteControllerDelegate {
func noteController(_ controller: NoteController, didSave text: String) {
store.update("note", text)
// pano alanını burada da yaz
}
}
Temsilci çağrısı iki işi bir araya getirir: sonucu sunana teslim eder ve sunanın ekranı yazması için bir yer açar. Çağrıyı yapan taraf temsilciyi atamadıysa çağrı sessizce atlanır; bu, ilk derste gördüğümüz bağlanmamış temsilci durumunun aynısıdır.
Sunum Biçimi ve Kapatma Yolları
Tam ekran biçimde sunan ekran görünümden kalkar ve sunulan kapanınca yeniden görünür; sayfa biçiminde ise alttaki ekran arkada görünmeye devam eder. Bu fark, yaşam döngüsü çağrılarının sunanda ne zaman çalışacağını değiştirir. Alttaki ekranın güncellemesini dönüş çağrısına bağlamak biçim değişince bozulur; güncellemeyi temsilci çağrısına bağlamak ise biçimden bağımsızdır.
Kullanıcı sayfayı aşağı çekerek kapattığında düğmenin eylemi çalışmaz. Kaydetme çağrısı yalnız düğmede duruyorsa bu yol kaydı atlar. isModalInPresentation = true yapmak aşağı çekmeyi engeller ve kullanıcıyı açık çıkışlara, yani düğmelere yönlendirir. Bir uyarı kutusu da aynı sunum çağrısıyla gösterilir; uyarı kapanınca sunanın ekranı yine kendi yazma çağrısına bağlıdır.
Sunulan denetleyiciye veri vermek gerektiğinde, veri sunmadan önce, present çağrısından önce atanır. Çağrıdan sonra atamak, sunulanın ilk gösteriminde boş veriyle açılmasına yol açabilir.
Sınırlayıcı Ölçüm
Ölçüm yalnız görünür ekrana bakar. Kapatılan not ekranının nesnesi bellekte durabilir ve içinde eski bir değer taşıyabilir, ama o ekran artık gösterilmediği için denetlenmez. Bu, ekranın ölçüm dışında olduğu anlamına gelir; olcum-disi bir alan değildir.
// main.swift — kapatilan ekran denetlenmez
final class TaskStore {
private(set) var values: [String: String] = [:]
func update(_ key: String, _ value: String) { values[key] = value }
func remove(_ key: String) { values[key] = nil }
}
final class TaskBoardController {
let screen = Screen("TaskBoardController")
private(set) var presented: String?
func present(_ name: String) { presented = name }
func dismiss() { presented = nil }
}
final class NoteController {
let store: TaskStore
let screen = Screen("NoteController")
init(store: TaskStore) { self.store = store }
func save(_ text: String) {
store.update("note", text)
screen.set("note", text)
}
}
let store = TaskStore()
let board = TaskBoardController()
let note = NoteController(store: store)
board.present("NoteController")
note.save("Ara")
board.dismiss()
store.remove("note")
print("sunulan: \(board.presented ?? "yok")")
print("kapanan ekranda kalan: \(note.screen.shown["note"] ?? "-")")
let visible = check("gorunen-pano", model: store.values, screen: board.screen, keys: ["note"])
countVerdicts(visible)
sunulan: yok kapanan ekranda kalan: Ara gorunen-pano | TaskBoardController.note: model=- ekran=- -> olcum-disi uyumlu: 0 bayat: 0 eksik: 0 artik: 0
Not ekranı kapandıktan sonra model notu sildi. Kapanan ekranın nesnesinde Ara hâlâ duruyor; çekirdek bu ekranı denetlemiş olsaydı satır artık verirdi. Ama sunulan ekran yok ve denetlenen tek ekran pano. Ölçüm bu yüzden sıfır sayı verir ve kapanan ekranın sapmasını raporlamaz. Ekranın ölçümden çıkması, ekranın modelle uyuştuğu anlamına gelmez.
Özet
- Kip sunum sunulan ekranı sunan ekranın üzerine koyar ve yığıta eklemez; kapatılınca sunan ekran olduğu gibi kalır.
- Sunulan ekranın girdiği değer modele yazılabilir ama sunan ekrana kendiliğinden ulaşmaz.
- Sunan ekranın hiç yazmadığı bir alan, modele girdiği anda eksik olur.
- Sunum sırasında değişmeyen alanlar sunan ekranda uyumlu kalır.
- Ölçüm yalnız görünür ekrana bakar; kapatılan ekran sayıma girmez.
- Bu dersin uyumu: eksik.
Sonraki Adım
Şimdiye kadar ekranları kodla kurduk: etiketler denetleyicinin alanlarıydı. Pek çok proje ekranı bir görsel düzenleyicide çizer ve etiketleri dosyadan bağlar. Sıradaki derste görsel düzenleyici ile kodla kurulan arayüzü karşılaştıracağız ve bağlantısı kopan bir etiketin sessizce yazılmadan kaldığı durumu ölçeceğiz.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.