Hi all
I'm reworking our app in SwiftUI. My ultimate goal is to access the NavigationPath from a child view which is used throughout different NavgationStacks. While searching for I came across different ways of achieving this. As I'm relatively new to SwiftUI it is hard to understand what the actual best practice seems to be.
So for the use case. My app has a TabView and each Tab has its own NavigationStack which looks something like this
struct TabNavigation: View {
@State private var selectedProductType: StaticProductType = .all
@StateObject private var appRouter = AppRouter()
var body: some View {
TabView(selection: $appRouter.selectedTab) {
Overview(activeType: $selectedProductType)
.tabItem {
Label("Home", systemImage: "house")
}
.tag(Tab.home)
AssortmentView(router: $appRouter.assortmentRouter,
activeType: $selectedProductType)
.tabItem {
Label(String(localized: "assortment"), systemImage:
"list.bullet")
}
.tag(Tab.assortment)
}
}
The AssortmenView holds the NavigationStack and defines the routes.
struct AssortmentView: View {
@Binding var router: AssortmentRouter
@Binding var activeType: StaticProductType
var body: some View {
NavigationStack(path: $router.navigationPath) {
VStack {
ProductTypeNavigation(activeType: $activeType)
.padding(.top, 10)
.padding(.horizontal, 10)
Spacer()
TabView(selection: $activeType) {
ListNavigation(type: .all)
.tag(StaticProductType.all)
ListNavigation(type: .games)
.tag(StaticProductType.games)
ListNavigation(type: .digital)
.tag(StaticProductType.digital)
ListNavigation(type: .toys)
.tag(StaticProductType.toys)
ListNavigation(type: .movies)
.tag(StaticProductType.movies)
ListNavigation(type: .books)
.tag(StaticProductType.books)
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
}
.addToolbar()
.navigationDestination(for: AssortmentRouter.Route.self) { route in
switch route {
case .overview(let type):
OverviewTypeView(type: type)
case .productDetail(let productId):
ProductDetailView(productId: productId)
.environmentObject(router)
case .productList:
ProductList()
}
}
}
}
}
Through my app I often use a view to displaying products. This view is reused over different NavigationStacks.
struct ProductDetailView: View {
var productId: Int
@StateObject private var viewModel: ProductDetailViewModel = ProductDetailViewModel()
@State private var showErrorAlert = false
@EnvironmentObject var router: AssortmentRouter
var body: some View {
VStack {
if !viewModel.isRefreshing {
let product = viewModel.product
VStack {
Text("Product: \(product.title)")
NavigationLink(destination: ProductDetailView(productId: Product.preview.productId)) {
Text("Test")
}
}
.navigationTitle(product.title)
} else {
ProgressView()
}
}.task {
await loadProduct()
}
.alert("Error", isPresented: $showErrorAlert, presenting: viewModel.localizedError) { _ in
Button("Try again") {
Task {
await loadProduct()
}
}
Button("Go Back", role: .cancel) {
// access navigationPath
}
} message: { errorMessage in
Text(errorMessage)
}
}
@MainActor
private func loadProduct() async {
await viewModel.loadProduct(productId: productId)
showErrorAlert = viewModel.localizedError != nil
}
}
In this example I created an AppRouter which holds all information for the routes and some functions to accessing the NavigationPath.
class AppRouter: ObservableObject {
var assortmentRouter = AssortmentRouter()
var selectedTab: Tab = .home
func navigateTo(tab: Tab) {
selectedTab = tab
}
}
class AssortmentRouter: ObservableObject {
var navigationPath = NavigationPath()
enum Route: Hashable {
case overview(type: StaticProductType)
case productList
case productDetail(productId: Int)
}
func navigateTo(route: Route) {
navigationPath.append(route)
}
}
This works fine as it is. The pro of this solution is that I don't have to pass the NavigationPath down each subview to use it as I can define it as EnvrionmentObject. The problem with this though, I like to reuse ProductDetailView also in my other NavigationStack which won't have a router binding of type AssortmentRouter
as you can imagine.
To come back to my initial question, what would be the best way to design this?
- Passing down a NavigationPath Binding and using different typing for navigationDestinaion values
- Define a callback which is passed as function parameter to the detail view
- Using dismiss, but I read that this is can lead to weird behaviour and bugs
- Any other option? Maybe changing the app architecture to handle this a better way
Apolgize the long post, but I would be really glad to get some feedback on this, so I can do it the right way.
Thank you very much