johnpatrickmorgan / FlowStacks

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

Not updating the view after e.g. coverScreen or push #39

Closed Kondamon closed 2 years ago

Kondamon commented 2 years ago

We have found out a strange behaviour that have occurred a few times now during implementation. We don't know if it's related to using multiple ObservableObjecs within the views and passing them around from the parent to a few childs or is it something else.

After a button click the routes are updated as expected but after the routes update e.g. via coverScreen, no rendering update happens in SwiftUI. So it looks like the if the button click doesn't have any effect on the UI. I have played around with using a custom .id(increasingNumber) on the related view and modified the id with the button click. After the 2nd click all buttons responds as expected again. However, do you have any idea why does this happen when using FlowStacks?

Kondamon commented 2 years ago

We have figured out the root cause of this issue. It is related to passing an ObservableObject with Routes from the parent to the child. Both hold the same instance of this object (see code example). However, this leads to not updating the view anymore when e.g. pushing a "view" to routes. Why is this happening and is it related to Node or a SwiftUI bug?

@main
struct FlowStacksApp: App {
  var body: some Scene {
    WindowGroup {
        ParentView()
    }
  }
}

struct ParentView: View {

    @ObservedObject var model = ChildModel()

    var body: some View {
        ChildView(model: getModel())
    }

    private func getModel() -> ChildModel {
        // When you comment out the next line, everything works fine
        model.routes = [.root(.home)]
        return model
    }
}

struct ChildView: View {
    enum Screen {
        case home, coverScreen
    }

    @ObservedObject var model = ChildModel()

    var body: some View {
        Router($model.routes) { screen, _ in
            switch screen {
            case .home:
                Button("Press here should show cover screen") {
                    model.routes.presentCover(.coverScreen)
                }
            case .coverScreen:
                Text("Cover screen successfully shown!")
            }
        }
    }
}

class ChildModel: ObservableObject {

    @Published var routes: Routes<ChildView.Screen>

    init(routes: Routes<ChildView.Screen> = [.root(.home)]) {
        self.routes = routes
    }

}
johnpatrickmorgan commented 2 years ago

Hi @Kondamon!

// When you comment out the next line, everything works fine
model.routes = [.root(.home)]

This line is the problem. The getModel() function is called whenever ParentView's body is evaluated. It mutates the model's routes property, which causes the ParentView's body to be re-evaluated. This continues in an infinite loop.

Even without the infinite loop, you would be continually resetting the routes state to the beginning whenever you tried to present a new screen:

Kondamon commented 2 years ago

Thank you, this helped me to resolve that issue!