Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mandatory pops to the root view when modifying observed object in the stacked view #38

Closed
xarple opened this issue Jan 21, 2021 · 6 comments

Comments

@xarple
Copy link

xarple commented Jan 21, 2021

Hi, this one of the great library I ever found. However when trying to modifying an observed object in the second view (stack view), then it pop to the root view immediately, here is the minimum code to reproduce the issue, please have a look:

import SwiftUI
import NavigationStack

class Model : ObservableObject {
    @Published var counter1 : Int = 0
    @Published var counter2 : Int = 0
}

struct SecondView: View {
    @ObservedObject var model : Model
    
    var body: some View {
        VStack() {
            Text("2st Page")
            Button(action: {
                model.counter2 += 1
                print(model.counter2)
            }, label: {
                Text("Press to increase (\(model.counter2))")
            })
            PopView(label: {
                Text("Back")
            })
        }
    }
}

struct ContentView: View {
    @ObservedObject var model : Model = Model()
    
    var body: some View {
        NavigationStackView() {
            VStack() {
                Text("1st Page")
                Button(action: {
                    model.counter1 += 1
                    print(model.counter1)
                }, label: {
                    Text("Press to increase (\(model.counter1))")
                })
                PushView(destination: SecondView(model : model), label: {
                    Text("Go to 2nd view")
                })
            }
        }
    }
}

When press the "Press to increase" button in the second view, the app pop to the root view immediately
Runs on Xcode 12.2 & iOS simulator iPhone 12 Pro, iOS 14.2

@xarple
Copy link
Author

xarple commented Jan 21, 2021

OK, seems that I found a little clue, the ContentView struct was being refactored once observed model are updated. The temporary solution is to declare the navigation stack within Model:

import SwiftUI
import NavigationStack

class Model : ObservableObject {
    @Published var counter1 : Int = 0
    @Published var counter2 : Int = 0
    @Published var stack = NavigationStack()
}

struct SecondView: View {
    @ObservedObject var model : Model
    
    var body: some View {
        VStack() {
            Text("2st Page")
            Button(action: {
                model.counter2 += 1
                print(model.counter2)
            }, label: {
                Text("Press to increase (\(model.counter2))")
            })
            PopView(label: {
                Text("Back")
            })
        }
    }
}

struct ContentView: View {
    @ObservedObject var model : Model = Model()

    var body: some View {
        NavigationStackView(navigationStack: model.stack) {
            VStack() {
                Text("1st Page")
                Button(action: {
                    model.counter1 += 1
                    print(model.counter1)
                }, label: {
                    Text("Press to increase (\(model.counter1))")
                })
                PushView(destination: SecondView(model : model), label: {
                    Text("Go to 2nd view")
                })
            }
        }
    }
}

The reason why refactoring occurs is still not sure though.

@brett-eugenelabs
Copy link

@StateObject would probably help you here, replace @ObservedObject var model : Model = Model() with @StateObject var model : Model = Model()

@matteopuc
Copy link
Owner

Hi @xarple what you are experiencing is something related to how SwiftUI works. When you change a Published property inside an ObservableObject you are basically asking SwiftUI to recompute the body of the views that depend on that ObservableObject. In your case your ContentView relies on Model and the ContentView body contains the NavigationStackView itself. So, what happens here is that you are recreating a new NavigationStackView each time you change your Model.
Usually, since the navigation stack manages the navigation throughout the app, what you should consider is moving the NavigationStackView to the entry point of your app (AppDelegate or SceneDelegate). For example:

import SwiftUI

@main
struct NavStackApp: App {
    var body: some Scene {
        WindowGroup {
            NavigationStackView {
                ContentView()
            }
        }
    }
}

and your ContentView would become:

struct ContentView2: View {
    @ObservedObject var model : Model = Model()

    var body: some View {
        VStack() {
            Text("1st Page")
            Button(action: {
                model.counter1 += 1
                print(model.counter1)
            }, label: {
                Text("Press to increase (\(model.counter1))")
            })
            PushView(destination: SecondView(model : model), label: {
                Text("Go to 2nd view")
            })
        }
    }
}

The solution you found is also valid, but it strictly depends on your needs. If it's ok for you to inject the NavigationStack into the NavigationStackView your solution is fine. The reason why it works is because it's true that each time you change your model you are recreating your content view and the NavigationStackView it contains, but this time you don't recreate the NavigationStack too. The NavigationStack is always the same (saved by yourself in the model).

@matteopuc
Copy link
Owner

@StateObject would probably help you here, replace @ObservedObject var model : Model = Model() with @StateObject var model : Model = Model()

Unfortunately, in this case, StateObject doesn't help us much. It's true that, as Apple suggests, you should go for StateObject instead of ObservedObject when the object is created by the view itself and not injected from the outside (as in this specific case). But the behaviour of the ObservableObject is the same: if it changes, the views that depend on it will be redrawn (i.e. their body will get recomputed).

@brett-eugenelabs
Copy link

@StateObject would probably help you here, replace @ObservedObject var model : Model = Model() with @StateObject var model : Model = Model()

Unfortunately, in this case, StateObject doesn't help us much. It's true that, as Apple suggests, you should go for StateObject instead of ObservedObject when the object is created by the view itself and not injected from the outside (as in this specific case). But the behaviour of the ObservableObject is the same: if it changes, the views that depend on it will be redrawn (i.e. their body will get recomputed).

I was commenting on his refactored version with having the NavigationStack inside the model. If his ContentView gets recreated then his model will get reinitialised, including the stack, and he will have the same problem. I know this because my use of NavigationStackView occurs a few screens deep into my App.

So I agree with the views will get redrawn with ObservableObject and StateObject, however the model itself will not get recreated using @StateObject.

I personally use @State in my App for the NavigationStack so I can pass it through bindings to its subviews, but it is the same effect as @StateObject.

Thanks for creating this Repo, my App wouldn't have been the same without it!

@matteopuc
Copy link
Owner

@brett-eugenelabs Yes, you're right. I thought you were commenting on the first version. Thank you so much for your words 🙏 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants