- Görkem Güray/
- 100 Günde SwiftUI Notları/
- 44.Gün - SwiftUI Navigation: Programmatic Navigation ve Path Kaydetme/
44.Gün - SwiftUI Navigation: Programmatic Navigation ve Path Kaydetme
Table of Contents
NavigationStack ile Programmatic Navigation #
Programmatic Navigation, kullanıcının herhangi bir eylemde bulunmasını beklemeden, programsal olarak tetiklenerek gerçekleşir. Örneğin bazı veriler işlendikten sonra kullancıyı sonuç ekranına götürebiliriz.
Bu SwiftUI’de NavigationStack
path’ini veriye bind ederek gerçekleştirilir.
struct ContentView: View {
@State private var path = [Int]()
var body: some View {
NavigationStack(path: $path) {
VStack {
// more code to come
}
.navigationDestination(for: Int.self) { selection in
Text("You selected \(selection)")
}
}
}
}
Buradaki // more code to come
kısmına iki adet buton ekleyelim;
Button("Show 32") {
path = [32]
}
Button("Show 64") {
path.append(64)
}
İlk buton ile tüm array’i değiştirerek sadece 32 sayısını içerecek şekilde ayarlıyoruz. Eğer array’de başka bir şey varsa kaldırılacaktır, yani NavigationStack
32 sayısına gitmeden önce orijinal durumuna geri dönecektir.
İkinci buton ile mevcut array’e 64 sayısını ekliyoruz, yani bu sayı gitmekte olduğumuz şeye eklenecektir. Dolayısıyla, array zaten 32 içeriyorsa, artık stack’de üç view olacaktır: Orijinal view (”root” olarak adlandırılır), ardından 32 sayısını gösteren bir şey ve son olarak 64 sayısını gösteren bir şey.
Aynı anda birden fazla değeri de push edebilirsiniz, bunun gibi;
Button("Show 32 then 64") {
path = [32, 64]
}
Bu, 32 için bir view ardından 64 için bir view sunacaktır, bu nedenle kullanıcının root view’e geri dönmek için iki kez Back butonuna dokunması gerekir.
NavigationPath Kullanarak Farklı Veri Türleri ile Çalışma #
Farklı veri türleri ile navigation iki şekilde gerçekleşir. En basit olanı, navigationDestination()
methodunu kullanarak farklı veri türlerini kullandığımız ancak gösterilen yolu tam olarak takip etmediğimiz durumdur, çünkü burada işler basittir: navigationDestination()
methodun istediğimiz her veri türü için olmak üzere çoklu olarak ekleyebiliriz.
Örneğin, beş sayı ve beş string gösterebilir ve bunlara farklı şekilde gidebiliriz.
NavigationStack {
List {
ForEach(0..<5) { i in
NavigationLink("Select Number: \(i)", value: i)
}
ForEach(0..<5) { i in
NavigationLink("Select String: \(i)", value: String(i))
}
}
.navigationDestination(for: Int.self) { selection in
Text("You selected the number \(selection)")
}
.navigationDestination(for: String.self) { selection in
Text("You selected the string \(selection)")
}
}
Ancak, programmatic navigation eklemek istediğimizde işler daha karmaşık hale gelir, çünkü navigation stack path’e bazı verileri bind etmemiz gerekir. path
değişkenine basit veri türlerini eklemeyi daha önce görmüştük, fakat daha kompleks veri türlerini işler biraz değişiyor.
SwiftUI’nin çözümü, tek bir path’de çeşitli veri türlerini tutabilen NavigationPath
adlı özel bir türdür. Pratikte bir array’e çok benzer şekilde çalışır.
Şu şekilde bir path
değişkeni oluşturabiliriz;
@State private var path = NavigationPath()
Şu şekilde NavigationStack
’e bind edelim;
NavigationStack(path: $path) {
Örnek olarak toolbar butonları ile programmatic olarak bir şeyler gösterelim;
.toolbar {
Button("Push 556") {
path.append(556)
}
Button("Push Hello") {
path.append("Hello")
}
}
NavigationStack’de Root View’e Nasıl Geri Dönülür #
Bir NavigationStack
’te bir kaç seviye derine indikten sonra başa dönmek isteyebiliriz. Örneğin, belki de kullanıcınız bir sipariş veriyordur ve sepetini gösteren, kargo bilgilerini, ödeme bilgilerini isteyen, ardından siparişi onaylayan ekranlar arasında ilerlemiştir, ancak iş bittiğinde en başa, NavigationStack
’in root view’ına geri dönmek isteyebilir.
Bunu göstermek içini her seferinde yeni rastgele sayılar üreterek yeni view’ları sonsuza kadar push eden küçük bir sanal alan oluşturabiliriz.
İlk olarak, mevcut numarasını başlık olarak gösteren ve her basıldığında yeni bir rastgele numaraya giden bir butona sahip DetailView’ımızı oluşturalım;
struct DetailView: View {
var number: Int
var body: some View {
NavigationLink("Go to Random Number", value: Int.random(in: 1...1000))
.navigationTitle("Number: \(number)")
}
}
Ve şimdi bunu ContentView’ımızdan sunabailiriz, başlangıç değeri 0 ile başlar ancak her yeni Int gösterildiğinde yeni bir DetailView
’a gider:
struct ContentView: View {
@State private var path = [Int]()
var body: some View {
NavigationStack(path: $path) {
DetailView(number: 0)
.navigationDestination(for: Int.self) { i in
DetailView(number: i)
}
}
}
}
Bunu çalıştırdığınızda, view’lar arasında sonsuza kadar ilerlemeye devam edebileceğinizi göreceksiniz.
Örneğin 10 view derine gittiyseniz root’a dönmek istediğinizde iki seçeneğimiz var;
- Yukarıdaki kodda yaptığımız gibi
path
için basit bir array kullanıyorsanız,path
’deki her şeyi kaldırmak için bu array üzerinderemoveAll()
işlevini çağırabilir ve root view’a geri dönebilirsiniz. path
içinNavigationPath
kullanıyorsanız, bunuNavigationPath
’in yeni, boş bir instance’ını yaparak halledebiliriz. Şu şekilde :path = NavigationPath()
Ancak daha büyük bir sorun var: orijinal path
property’ye erişimimiz olmadığında bunu alt view’dan nasıl yapabiliriz?
Burada iki seçeneğimiz var: path
’i @Observable
kullanan harici bir sınıfta saklamak ya da @Binding
adında yeni bir property wrapper kullanmak. Daha önce @Observable
’ı incelemiştik, o yüzden burada @Binding
’e odaklanalım.
Uygulamamız çalışırken değerleri değiştirebilmemiz için @State
’in view’ın içinde bir depolama alanı oluşturmamıza nasıl izin verdiğini gördük. @Binding
property wrapper, bir @State
property’yi başka bir view’a aktarmamıza ve oradan değiştirmemize olanak tanır. Yani @State
property’yi birkaç yerde paylaşabiliriz ve bir yerde değiştirmek onu her yerde değiştirir.
Mevcut kodumuzda bu, navigation path array’e erişmek için DetailView
’e yeni bir property eklemek anlamına geliyor.
@Binding var path: [Int]
Ve şimdi bunu ContentView
’da DetailView
’in kullanıldığı her iki yerden de şu şekilde geçmemiz gerekiyor.
DetailView(number: 0, path: $path)
.navigationDestination(for: Int.self) { i in
DetailView(number: i, path: $path)
}
Gördüğünüz gibi $path
değişkenini geçiyoruz çünkü binding yapmak istiyoruz. Yani DetailView’in path
’i okuyup yazabilmesini istiyoruz.
Ve şimdi path
array’i değiştirmek için DetailView
’e bir toolbar ekleyebiliriz.
.toolbar {
Button("Home") {
path.removeAll()
}
}
Ve tabiki NavigationPath
kullanıyorsanız bunu kullanırsınız;
.toolbar {
Button("Home") {
path = NavigationPath()
}
}
Bu şekilde binding yapmak yaygındır. TextField
, Stepper
gibi kontroller tam olarak bu şekilde çalışır.
Codable Kullanarak NavigationStack Path Nasıl Kaydedilir? #
Codable kullanarak navigation stack path’i 2 farklı yoldan biriyle kaydedebilir ve yükleyebiliriz. Bu seçim path’in türüne bağlıdır.
NavigationStack
’imizin aktif path’ini saklamak içinNavigationPath
kullanıyorsanız, SwiftUI yollarınızı kaydetmeyi ve yüklemeyi kolaylaştırmak için iki yardımcı sağlar.- Homojen bir array kullanıyorsanız örneğin
[Int]
veya[String]
, bu yardımcılara ihtiyacınız yoktur ve verilerinizi özgürce yükleyebilir veya kaydedebilirsiniz.
Teknikler çok benzer, bu yüzden burada ikisini de ele alacağız.
Her ikisi de path’i view dışında depolamaya dayanır, böylece path verilerinin yüklenmesi ve kaydedilmesi görünmez bir şekilde gerçekleşir, yani external bir sınıf bunu otomatik olarak halleder. Daha spesifik olmak gerekirse, path verilerimiz değiştiğinde (int, string veya NavigationPath nesnesi) yeni path’i kaydetmemiz gerekir, böylece gelecekte saklanır ve sınıf başlatıldığında bu verileri diskten geri yükleriz.
Path verilerimiz bir Int array olarak saklandığında bu sınıfın nasıl görüneceği aşağıda açıklanmıştır;
@Observable
class PathStore {
var path: [Int] {
didSet {
save()
}
}
private let savePath = URL.documentsDirectory.appending(path: "SavedPath")
init() {
if let data = try? Data(contentsOf: savePath) {
if let decoded = try? JSONDecoder().decode([Int].self, from: data) {
path = decoded
return
}
}
// Still here? Start with an empty path.
path = []
}
func save() {
do {
let data = try JSONEncoder().encode(path)
try data.write(to: savePath)
} catch {
print("Failed to save navigation data")
}
}
}
NavigationPath
kullanıyorsanız, yalnızca dört küçük değişikliğe ihtiyacınız vardır.
İlk olarak, path
property’nin [Int]
yerine NavigationPath
türüne sahip olması gerekir.
var path: NavigationPath {
didSet {
save()
}
}
İkinci olarak, initializer’da JSON decode ettiğimiz yerde gerekli değişikliği yapmamız gerekiyor.
if let decoded = try? JSONDecoder().decode(NavigationPath.CodableRepresentation.self, from: data) {
path = NavigationPath(decoded)
return
}
Üçüncü olarak, decode işlemi başarısız olursa, initializer’ın sonundaki path
property yeni boş bir NavigationPath
instance atamalıyız;
path = NavigationPath()
Ve son olarak, save()
methodu navigation path’in Codable
temsilini yazması gerekir. Burada basit bir array kullanmaktan biraz daha fazlasını yapmalıyız, çünkü NavigationPath
veri tiplerinin Codable
’a uygun olmasını gerektirmiyor (sadece Hashable
’a uygunluğuna ihtiyaç duyuyor). Sonuç olarak, Swift derleme zamanında navigation path’i geçerli bir Codable temsili olduğunu doğrulayamaz, bu nedenle bunu talep etmemiz ve ne geri geleceğini görmemiz gerekir.
Bu da save()
methodunun başına, Codable
navigation path’i almaya çalışan ve geri dönüş alamazsak hemen iptal eden bir kontrol eklemek anlamına geliyor.
guard let representation = path.codable else { return }
Bu, JSON’a encode edilmeye hazır verileri döndürür ya da path’deki en az bir nesne encode edilemezse nil
döndürür.
Son olarak, bu Codable
gösterimini orijinal Int
dizisi yerine JSON’a dönüştürüyoruz.
İşte sınıfın tamamlanmış hali böyle gözüküyor.
@Observable
class PathStore {
var path: NavigationPath {
didSet {
save()
}
}
private let savePath = URL.documentsDirectory.appending(path: "SavedPath")
init() {
if let data = try? Data(contentsOf: savePath) {
if let decoded = try? JSONDecoder().decode(NavigationPath.CodableRepresentation.self, from: data) {
path = NavigationPath(decoded)
return
}
}
// Still here? Start with an empty path.
path = NavigationPath()
}
func save() {
guard let representation = path.codable else { return }
do {
let data = try JSONEncoder().encode(representation)
try data.write(to: savePath)
} catch {
print("Failed to save navigation data")
}
}
}
Artık SwiftUI kodumuzu normal şekilde yazabilir ve NavigationStack
’imizin path’ini bir PathStore
instance’ının path
property’sine bind ettiğimizden emin olabiliriz. Bu sayede rastgele tamsayılar eklenmiş view’lar gösterebiliriz, istediğimiz kadar view gönderebilir, ardından uygulamayı tam olarak bıraktığımız gibi geri almak için sessizce yeniden başlatabiliriz.
struct DetailView: View {
var number: Int
var body: some View {
NavigationLink("Go to Random Number", value: Int.random(in: 1...1000))
.navigationTitle("Number: \(number)")
}
}
struct ContentView: View {
@State private var pathStore = PathStore()
var body: some View {
NavigationStack(path: $pathStore.path) {
DetailView(number: 0)
.navigationDestination(for: Int.self) { i in
DetailView(number: i)
}
}
}
}
Bu yazıyı İngilizce olarak da okuyabilirsiniz.
You can also read this article in English.