QuickBirdEng / SwiftUI-Coordinators-Example

Sample app that showcases the use of the Coordinator Pattern in SwiftUI
MIT License
223 stars 22 forks source link

More than 3 levels of hierarchy #3

Closed karlkaminski closed 2 years ago

karlkaminski commented 2 years ago

I am in a project that uses your coordinator pattern.

For a new requirement there must be a deep level hierarchy. E.g.: First level is a recipe list like in your example. In the detail view of a recipe is a list with related ingredients. (Eggs...) The user can now touch on a related ingredients item and a new list with recipes with eggs will be shown Now the user can go to a detail recipe and there is again a list and so on.

So it could be an unlimited deep navigation stack.

After several hours to fulfill the requirement we decided, that a static deep of 4 or 5 levels could be ok.

With your coordinator pattern I have an issue with more than 3 levels.

The fourth level will not be opened, instead the view navigates to the second level.

I created a very small example app. No side effects can occurre. So in my opinion your coordinator can not handle more that tree level of hierarchy. It is a bug or designed?

Example: Coordinator:

import SwiftUI

class MainCoordinator: ObservableObject, Identifiable {

    @Published var firstLevelViewModel: FirstLevelViewModel!
    @Published var secondLevelViewModel: SecondLevelViewModel?
    @Published var thirdLevelViewModel: ThirdLevelViewModel?
    @Published var fourthLevelViewModel: FourthLevelViewModel?

    init() {
        self.firstLevelViewModel = .init(coordinator: self)
    }

    func navigateToSecond() {
        self.secondLevelViewModel = .init(coordinator: self)
    }

    func navigateToThird() {
        self.thirdLevelViewModel = .init(coordinator: self)
    }

    func navigateToFourth() {
        self.fourthLevelViewModel = .init(coordinator: self)
    }

}

CoordinatorView:

struct MainCoodinatorView: View {

    @ObservedObject var coordinator: MainCoordinator

    var body: some View {
        NavigationView {
            FirstLevelView(viewModel: coordinator.firstLevelViewModel)
                .navigation(item: $coordinator.secondLevelViewModel) { viewModel in
                    SecondLevelView(viewModel: viewModel)
                        .navigation(item: $coordinator.thirdLevelViewModel) { viewModel in
                            ThirdLevelView(viewModel: viewModel)
                                .navigation(item: $coordinator.fourthLevelViewModel) { viewModel in
                                    FourthLevelView(viewModel: viewModel)
                                }
                        }
                }
        }
    }

}

Every View:

import SwiftUI

struct SecondLevelView: View {
    var viewModel: SecondLevelViewModel

    var body: some View {
        VStack {
            Text("Second Level")
            Button(action: {
                viewModel.goToThird()
            }, label: {
                Text("To Third")
            })
        }
    }
}

ViewModel:

import Foundation

class SecondLevelViewModel: ObservableObject, Identifiable {

    private unowned let coordinator: MainCoordinator

    init(coordinator: MainCoordinator) {
        self.coordinator = coordinator
    }

    func goToThird() {
        coordinator.navigateToThird()
    }

}

The complete code: MultiLevelNavigation.zip

pauljohanneskraft commented 2 years ago

Hey @karlkaminski - I just tried it and this seems to be related to the default navigation view style of iOS. If you specifically set the navigationViewStyle to .stack, it works as expected. I assume that UISplitViewControllers only support three children - could that be right?

Edit: To be a little more precise, this can easily be done by changing the MainCoordinatorView to the following:

struct MainCoodinatorView: View {

    @ObservedObject var coordinator: MainCoordinator

    var body: some View {
        NavigationView {
            FirstLevelView(viewModel: coordinator.firstLevelViewModel)
                .navigation(item: $coordinator.secondLevelViewModel) { viewModel in
                    SecondLevelView(viewModel: viewModel)
                        .navigation(item: $coordinator.thirdLevelViewModel) { viewModel in
                            ThirdLevelView(viewModel: viewModel)
                                .navigation(item: $coordinator.fourthLevelViewModel) { viewModel in
                                    FourthLevelView(viewModel: viewModel)
                                }
                        }
                }
        }
        .navigationViewStyle(.stack)
    }

}

Btw: You will need to wrap your view models with the ObservedObject property wrapper to make them work properly.

karlkaminski commented 2 years ago

Thank you for you help. Do you have an advice, how I could create an unlimited navigation stack? I try it by myself and come back later if it's ok?

pauljohanneskraft commented 2 years ago

What do you mean by that exactly? Do you mean stacking infinitely many of the same view but with different view models or something like that?

karlkaminski commented 2 years ago

Yes, like I wrote in the first text.

First level is a recipe list like in your example. In the detail view of a recipe is a list with related ingredients. (Eggs...) The user can now touch on a related ingredients item and a new list with recipes with eggs will be shown Now the user can go to a detail recipe and there is again a list and so on.

So it could be an unlimited deep navigation stack.

List of Recipes -> Recipe -> List of Recipes for Eggs -> Recipe -> List of Recipes with Milk -> Recipe ... Or we have articles with a list of related articles in the article: Article1 (ArticleDetailViewModel with articleID = x) -> Article2 (ArticleDetailViewModel with articleID = y) -> Article1 (ArticleDetailViewModel with articleID = z)...

pauljohanneskraft commented 2 years ago

I would personally argue against this due to possible memory issues and non-intuitive UI, but if that would be your goal, then this would be my solution:

class ArticleCoordinator: ObservableObject {

    @Published var selectedArticle: ArticleCoordinator?

}

struct ArticleCoordinatorView: View {

    @ObservedObject var coordinator: ArticleCoordinator

    var body: some View {
        Text("hello")
            .navigation(item: $coordinator.selectedArticle) {
                ArticleCoordinatorView(coordinator: $0)
            }
    }

}

The NavigationView would then need to be applied around it. If you prefer an array of view models, you can try out how to make that work. I will check, whether I can open source my own implementation of that.

For iOS 16, you could use the NavigationStack to do exactly that.

class ArticleCoordinator: ObservableObject {
     @Published var articles = [ArticleViewModel]()

      func selectNewArticle() {
           articles.append(.init(coordinator: self))
      }
}

struct ArticleCoordinatorView: View {

    @ObservedObject var coordinator: ArticleCoordinator

    var body: some View {
        NavigationStack(path: $coordinator.articles) {
            Button("Select new article") {
                 coordinator.selectNewArticle()
            }
            .navigationDestination(for: ArticleViewModel.self) {
               ArticleView(viewModel: $0)
            }
        }
    }

}
karlkaminski commented 2 years ago

Thank you very much. I think I understand the Coordinator now better with your ArticleCoordinator. Our min iOS is iOS14, but I saw the new NavigationStack and hope, we are waiting not to long to increase the min iOS version.