johnpatrickmorgan / FlowStacks

FlowStacks allows you to hoist SwiftUI navigation and presentation state into a Coordinator
MIT License
783 stars 56 forks source link

Nested coordinators. Fatal error: No view builder found for type #75

Closed lisindima closed 3 weeks ago

lisindima commented 3 weeks ago

Hi, thanks for the great library. I'm trying to implement navigation with nested coordinators. My project is divided into modules, each module has its own coordinator, and there is also a root coordinator that creates all the others. There is an example of code that causes an error: Fatal error: No view builder found for type NewPath. I provide you with a minimal example to reproduce the error

enum Path: Hashable {
    case one(Int)
}

enum NewPath: Hashable {
    case one(String)
}

class FirstCoordinator: ObservableObject {
    @Published var path: Routes<Path> = []

    func push() {
        path.push(.one(1))
    }
}

struct ContentView: View {
    @ObservedObject var coordinator: FirstCoordinator

    var body: some View {
        FlowStack($coordinator.path, withNavigation: true) {
            VStack {
                Button {
                    coordinator.push()
                } label: {
                    Text("Push")
                }
                Text("ROOT")
            }
            .flowDestination(for: Path.self) { screen in
                switch screen {
                case let .one(value):
                    TestView(coordinator: .init(), value: value)
                }
            }

        }
    }
}

class SecondCoordinator: ObservableObject {
    @Published var path: Routes<NewPath> = []

    func push() {
        path.push(.one("exception"))
    }
}

struct TestView: View {
    @ObservedObject var coordinator: SecondCoordinator

    let value: Int

    var body: some View {
        FlowStack($coordinator.path) {
            VStack {
                Button {
                    coordinator.push()
                } label: {
                    Text("push")
                }
                Text(value.description)
            }
            .flowDestination(for: NewPath.self) { screen in
                switch screen {
                case let .one(value):
                    Test2View(value: value)
                }
            }
        }
    }
}

struct Test2View: View {
    let value: String
    var body: some View {
        Text(value)
    }
}

Is this a library bug or a bug in my implementation?

johnpatrickmorgan commented 3 weeks ago

Thanks for raising this @lisindima - it was indeed a bug in the library code. I have made a change in v0.6.2 that should resolve the issue.

Side note: there is another issue with the example code you posted:

.flowDestination(for: Path.self) { screen in
   switch screen {
   case let .one(value):
     TestView(coordinator: .init(), value: value)
   }
}

Because you're initialising a new coordinator each time this closure is called, the child coordinator will soon get replaced by one with an empty path, popping the screen. You could prevent this by including the coordinator in the parent's path (e.g. case one(Int, SecondCoordinator)) or by keeping hold of it in the first coordinator, so that it remains stable. It seems like NavigationStack behaves differently though (see https://github.com/johnpatrickmorgan/NavigationBackport/issues/47), so there should maybe be a change in this library to not call the closure multiple times.

lisindima commented 3 weeks ago

Thank you very much for the quick fix!