SwiftUI | Strange behaviour after appending paths to Routerpath onAppear

490 views Asked by At

I want to completely rework how NavigationBar's look and rework back button as I like and have my own router In my app.

I created this sample project to understand how NavigationStack and NavigationPath works before starting my project (From this gist)

It all works as expected; until I push some views at onAppear stage.

  1. I hid the back button and added my own which mimics the default back button.
  2. I tap on list items, app pushes them as expected.
  3. My custom back button removes them as expected.

Now, I added onAppear() {...} to push 4 views when the app runs (trying to mimic that user opens a notification and app directs the user to the corresponding view)

The expected behavior is to have 4 views in navigationpath stack and my custom back button removes them.

But app acts like there is no views in stack, tapping on my custom back button will crash the app. (You will see screen count 0 text in app) I have print() statements in onAppear and they say I have 4 views in stack.

If I remove my custom back button and show the default back button; yep, it works, app acts as 4 views in stack. (But the in app screen counter still will say 0 views in stack)

Here is the code; run it in two ways to see the problem:

  1. Run as it is.
  2. Comment out the .toolbar() { ... } and .navigationBarBackButtonHidden() and run

I don't know if I misunderstood the concept or if there is a bug.

BTW; Xcode preview works as I expected. It is a runtime problem.

Thank you for your time.

import SwiftUI

final class Router: ObservableObject {
    @Published var path = NavigationPath()
}

struct ContentView: View {
    
    @StateObject var router: Router = Router()
    
    var body: some View {
        NavigationStack(path: $router.path) {
            RowListsView()
                .navigationDestination(for: Int.self) { i in
                    RowListsView()
                        .navigationBarBackButtonHidden()
                        .toolbar(content: {
                            if router.path.count > 0 {
                                ToolbarItem(placement: .navigationBarLeading) {
                                    Button("MyBack") {
                                        router.path.removeLast()
                                    }
                                }
                            }
                        })
                }
        }
        .environmentObject(router)
        .onAppear(perform: {
            print("before", router.path.count)
            router.path.append(1)
            router.path.append(2)
            router.path.append(3)
            router.path.append(4)
            print("after", router.path.count)
        })
    }
}

struct RowListsView : View{
    
    @EnvironmentObject var router: Router
    
    var body: some View {
        
        Form{
            List(1..<5, id: \.self) { i in
                NavigationLink(value: i) {
                    Text("\(i)")
                }
            }
            Section{

                    Button("Screen count \(router.path.count)"){
                        print("")
                    }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    
    static var previews: some View {
        ContentView()
    }
}
1

There are 1 answers

1
Martin On

This is interesting case. I tried your code in Swift Playground. When path count is 0 and I hit Back button, app crashes too. To init @State or @StateObject objects in .onAppear is not correct/recommended way.

Solution:
Remove your .onAppear {..} code and app will start with no back button and it is working fine. When you want initial values for path, try different way. For example following code uses simple init in Router class.

Working code:
When you hit Back button and path count goes to 0, Back button will disappear, which is correct.

import SwiftUI

class Router: ObservableObject {
    @Published var path = NavigationPath()
    
    init() {
        self.path.append(1)
        self.path.append(2)
        self.path.append(3)
        self.path.append(4)
    }
}

struct ContentView: View {
    @StateObject var router: Router = Router()
    
    var body: some View {
        NavigationStack(path: $router.path) {
            RowListsView(router: router)
                .navigationDestination(for: Int.self) { i in
                    RowListsView(router: router)
                        .navigationBarBackButtonHidden()
                        .toolbar(content: {
                            if router.path.count > 0 {
                                ToolbarItem(placement: .navigationBarLeading) {
                                    Button("MyBack") {
                                        router.path.removeLast()
                                    }
                                }
                            }
                        })
                }
        }
        .environmentObject(router)
    }
}

struct RowListsView : View{
    @ObservedObject var router: Router
    
    var body: some View {
        Form{
            List(1..<5, id: \.self) { i in
                NavigationLink(value: i) {
                    Text("\(i)")
                }
            }
            Section{
                Button("Screen count \(router.path.count)"){
                    print("")
                }
            }
        }
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}