Skip to content
Banner image

SwiftUI Programmatic Routing dengan NavigationStack

Authored on December 25, 2023 by Aaron Christopher.

6 min read
--- views

Perkenalan

Setelah absen panjang tidak merilis konten blog baru, saya kembali dengan konten iOS. Saya telah mendapatkan wawasan berharga tentang iOS dan Pemrograman Swift di Apple Developer Academy. Sekarang, dengan pembaruan iOS 17, kita memiliki makro baru Observable dan Framework Observable. Selain itu, sistem navigasi terbaru yang diperkenalkan di iOS 16, NavigationStack, sangatlah keren, dan kita akan memanfaatkannya sebaik mungkin.

Penggunaan NavigationStack

Implementasi biasa dari NavigationStack adalah dengan menempatkan NavigationLink untuk memungkinkan pengguna menavigasi dengan menekannya.

struct ContentView: View { let products: [Product] var body: some View { NavigationStack { List(products) { product in NavigationLink(product.title) { ProductDetailView(product: product) } } .navigationTitle("Products") } } } struct ProductDetailView: View { let product: Product var body: some View { Text(product.title) .font(.title) .navigationTitle(product.title) } }
swift

Pada contoh di atas, kita menyiapkan konfigurasi dasar master-detail. Kita menempatkan NavigationStack di bagian atas tata letak tampilan kita. Selanjutnya, kita menuliskan daftar message, setiap berfungsi sebagai tautan ke layar detailnya. Perhatikan, kita tetap menggunakan tipe NavigationLink tradisional, dan hal ini berfungsi dengan baik di sini.

Saya menikmati mengorganisir alur navigasi fitur saya di satu lokasi pusat. Itulah mengapa saya biasanya menggunakan pola Navigator untuk mengelola navigasi dengan cara yang aman berdasarkan tipe. Mengimplementasikan pola Navigator dengan API Navigasi baru SwiftUI mudah dilakukan. Pada awalnya, kita perlu membuat tipe enum yang menguraikan semua rute untuk aplikasi, fitur, atau modul kita.

enum Route: Hashable { case first case second case third case fourth }
swift

Selanjutnya, kita mendefinisikan tampilan berdasarkan rute yang kita tentukan sebelumnya.

struct Routes: View { let route: Route var body: some View { switch route { case .first: FirstView() case .second: SecondView() case .third: ThirdView() case .fourth: FourthView() } } }
swift

Setelah itu, kita seharusnya membuat satu pengontrol tunggal untuk navigasi. Ini dapat dilakukan dengan menggunakan ViewModel. Ingat ketika saya menyebutkan sebelumnya bahwa kita dapat memanfaatkan Observable Framework baru? Kita akan mendefinisikan ViewModel dengan menggunakan makro Observable.

@Observable class RouteViewModel { static var shared = RouteViewModel() var navPath = [Route]() { willSet { previousRoute = navPath.last } } private(set) var previousRoute: Route? var currentRoute: Route? { navPath.last } // MARK: - Append route to navigation path /// Append AKA go to next route func append(_ route: Route, before: (() -> Void)? = nil) { before?() navPath.append(route) } // MARK: - Pop route from navigation path /// Pop AKA return to previous view in the navigation stack func pop(before: (() -> Void)? = nil) { guard !navPath.isEmpty else { print("navPath is empty") return } before?() navPath.removeLast() } // MARK: - Pop multiple routes /// Return to previous view multiple times in the navigation stack func pop(_ count: Int, before: (() -> Void)? = nil) { guard navPath.count >= count else { print("count must not be greater than navPath.count") return } before?() navPath.removeLast(count) } // MARK: - Pop to root /// Back to root view func popToRoot(before: (() -> Void)? = nil) { before?() navPath.removeLast(navPath.count) } // MARK: - Append multiple routes /// Append multiple routes to navigation stack func append(_ routes: Route..., before: (() -> Void)? = nil) { before?() routes.forEach { route in navPath.append(route) } } }
swift

Sepertinya kode yang cukup panjang, bukan? Variabel navPath adalah sumber kebenaran, menyimpan daftar route. Ini adalah array route dan kita dapat memodifikasinya seperti array lainnya. Kita telah menentukan beberapa metode di sana untuk membantu kita dalam menavigasi dan memodifikasi navPath itu sendiri. ViewModel ini akan digunakan secara global di seluruh aplikasi, jadi kita harus membuat Environment.

Mendefinisikan EnvironmentValues

Untuk mendefinisikan Environment navigate, kita dapat melakukan extend EnvironmentValues dari SwiftUI. Tetapi pertama, kita perlu mendefinisikan EnvironmentKey dengan defaultValue dari ViewModel itu sendiri.

struct NavigationEnvironmentKey: EnvironmentKey { static var defaultValue: RouteViewModel = .init() }
swift

Setelah itu, kita dapat mengextend EnvironmentValues. Saya memberinya nama navigate, tetapi Anda dapat menggunakan nama lain sesuai keinginan Anda.

extension EnvironmentValues { var navigate: RouteViewModel { get { self[NavigationEnvironmentKey.self] } set { self[NavigationEnvironmentKey.self] = newValue } } }
swift

Sekarang kita dapat menggunakan environment secara global. Selanjutnya, kita perlu menggunakan ViewModel dalam NavigationStack. Dalam implementasi ini, kita akan melakukannya di dalam struktur App.

Mengimplementasikan NavigationStack

Kita harus menggunakan Bindable macro alih-alih State karena NavigationStack perlu untuk melakukan bind pada NavigationPath.

struct SwiftRoutingApp: App { @Bindable private var routeViewModel = RouteViewModel.shared var body: some Scene { WindowGroup { NavigationStack(path: $routeViewModel.navPath) { ContentView().navigationDestination(for: Route.self) { route in Routes(route: route) } }.environment(\.navigate, routeViewModel) } } }
swift

Sekarang kita memiliki satu NavigationStack yang membungkus ContentView, dan kita melampirkan view modifier navigationDestination serta menggunakan struktur Routes yang sebelumnya telah kita tentukan untuk memetakan tampilan di dalam NavigationStack. Jangan lupa menggunakan view modifier environment agar dapat mengakses view model di dalam child view.

Penggunaan

Anda seharusnya menyadari bahwa dalam pemetaan Routes yang sebelumnya telah ditentukan, ada beberapa views di sana, sekarang kita akan menggunakannya untuk menunjukkan sistem navigasi baru kita. Tampilan itu sendiri tidak terlalu kompleks tetapi sudah cukup untuk keperluan demonstrasi.

struct FirstView: View { @Environment(\.navigate) private var navigate var body: some View { Text("First View") Button("To second view") { navigate.append(.second) } } }
swift
struct SecondView: View { @Environment(\.navigate) private var navigate var body: some View { Text("Second View") Button("To third view") { navigate.append(.third) } Button("Back to root") { navigate.popToRoot() } } }
swift
struct ThirdView: View { @Environment(\.navigate) private var navigate var body: some View { Text("Third View") Button("To fourth view") { navigate.append(.fourth) } Button("Pop two view") { navigate.pop(2) } Button("Back to root") { navigate.popToRoot() } } }
swift
struct FourthView: View { @Environment(\.navigate) private var navigate var body: some View { Text("Fourth View") Button("Pop") { navigate.pop() } Button("Pop to second view") { navigate.pop(2) } Button("Back to root") { navigate.popToRoot() } } }
swift
struct ContentView: View { var body: some View { FirstView() } }
swift

Ketika Anda menjalankan aplikasi, tampilan pertama akan muncul. Tekan tombol untuk navigasi ke tampilan kedua. Sekarang, cobalah berinteraksi dengan semua tombol dan lihat seberapa keren sistem ini. Anda tidak perlu menggunakan NavigationLink di mana-mana; cukup panggil metode navigate. Anda juga dapat memanggil ini di dalam fungsi atau pemicu apa pun.

Tentu saja, source codenya tersedia di GitHub.

Kesimpulan

Saya sangat antusias menggunakan API Navigasi berbasis data baru di SwiftUI. Semoga Anda menikmati tulisan ini. Jangan ragu untuk menghubungi saya dan ajukan pertanyaan terkait tulisan ini. Terima kasih telah membaca, dan sampai jumpa di blog berikutnya!