johnpatrickmorgan / FlowStacks

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

NavigationView state gets reset when presenting and dismissing view on NavigationView #40

Closed Kondamon closed 1 year ago

Kondamon commented 1 year ago

Thank you for this excellent framework!

We have found an issue when using an UIViewControllerRepresentable with a popover. The UIVIewController present a popover when it appears. After clicking on the popover and dismissing it, a new rendering cycle starts and the NavigationView pops back to its root view.

We have tried to debug the issue and set a breakpoint on the switch statement. Theroutes just hold a single element instead of 2 routes (root and the pushed view). It seems that after the routes object is updated no rendering cycle occurs were there are 2 elements in routes.

In another debugging attempt we replaced the routes with a ButtonView and everything works fine. However also here we have seen that the routes object has 1 element after init (when evaluating it at the before mentioned breakpoint), after pushing a 2nd view it still shows only 1 element, after another push there are 3 elements in the routes object.

public struct Content: View {

    @State private var routes: Routes<Screen>

    let onDismissTapped: (() -> Void)

    public var body: some View {
        Router($routes) { screen, _ in
            switch screen {
            case .rootView:
                let model = Model()
                RootView(model: model)
            case .pushedView:
                let model = Model2(didSelectNextButton: clickedNext)
                ViewRepresentableView(model: model) // this one is presenting a popover when it appeares
            case .thirdView:
                ThirdView()
            }
        }
        .navigationBarTitle(Text(" "), displayMode: .inline)
    }
}

struct ButtonView: View {
    var action: () -> Void
    let date = Date()

    var body: some View {
        Button("press me! \(date.asStringISO8601)") {
            action()
        }
    }
}
johnpatrickmorgan commented 1 year ago

Thanks for raising this issue @Kondamon! I haven't yet tried to reproduce the issue, but something seems strange in your example code:

let model = Model()
RootView(model: model)

Is model an ObservableObject? If so, I don't think it's a good idea to recreate it every time the coordinator's body is re-evaluated (which will happen frequently). Such a model should last from when its screen is first created to when it is destroyed. Otherwise, SwiftUI will consider the view to require re-rendering every time the body is evaluated. This can cause the strange behaviour you've noticed.

If you were to include the model as an associated value of your screen enum, then the same model instance will be used for that screen even when the body is re-evaluated, e.g.:

Router($routes) { screen, _ in
  switch screen {
  case .rootView(let model):
    RootView(model: model)
  case .pushedView(let model):
    ViewRepresentableView(model: model)
  case .thirdView:
    ThirdView()
  }
}