swiftui

SwiftUI animations with programmable navigation when the view hierarchy doesn't change


I am writing an anki-like app with language exercises. Completing an exercise leads you to the exercise with the next word, and so on, indefinitely. I want to transition between words using navigation. This is a simplified example of what I have come up with:

struct ContentView: View {
    @State private var page: Int = 0
    @State private var path = NavigationPath()

    var body: some View {
        NavigationStack(path: $path) {
            VStack {
                NavigationLink("Go to page \(page)", value: page)
            }
            .navigationDestination(for: Int.self) { currentPage in
                VStack {
                    Text("You selected \(currentPage)")
                    Button("Next page") {
                        page += 1
                        path.removeLast()
                        path.append(page)
                    }
                }.onAppear() {
                    print("View with page \(page) is appearing")
                }
            }
        }
    }
}

I use NavigationPath with pushing and popping to "replace" the uppermost view with the next word exercise. Note that I don't want to just "push" the view into the stack, because then it will grow indefinitely. I also want the "back" button to lead to the main menu, not the previous exercise. The code provided works, but there is no animation when transitioning to next page. Also, the "onAppear" method is not called when navigating to the next page.

Suspecting that it has to do with view hierarchy not changing, I've tried make id of the VStack view depend on the page using .id modifier, and also pushing different types of objects into path - none of these helped.

Would be really grateful for any ideas on how to fix this!


Solution

  • The way you had it, there is no navigation happening. You're not going to or creating new views, you're just updating values in the same view (ContentView). Logically, there is no navigation transition animation because there is no navigation. This is also the reason why .onAppear doesn't trigger, since the root view never disappears.

    To actually navigate, create a new struct for the new page that accepts necessary values like page and path. Then, call it in the .navigationDestination.

    Since your navigation model is simple, based on numbers, you can make your life easier by using an array of Int as your path, instead of a NavigationPath type, which I believe allows for type erasure - meaning it can hold anything, not just Int types. The code below uses an array of Int as an example.

    There is no need for pushing and popping to manage the path stack if you only need to go back to the root. You can keep it simple and just keep pushing until you pop everything to go to the root view.

    Give this a try:

    import SwiftUI
    
    struct PopNavigation: View {
        
        //State values
        @State private var page: Int = 1
        @State private var path: [Int] = []
        
        //Body
        var body: some View {
            NavigationStack(path: $path) {
                VStack {
                    NavigationLink("Go to page \(page)", value: page)
                }
                .navigationDestination(for: Int.self) { _ in
                    NavPageView(page: $page, path: $path)
                        
                }
                .navigationTitle("Main")
            }
        }
    }
    
    struct NavPageView: View {
        
        //Parameters
        @Binding var page: Int
        @Binding var path: [Int]
        
        //Body
        var body: some View {
            VStack {
                Text("You selected \(page)")
                
                Button {
                    page += 1
                    path.append(page)
                } label: {
                    Text("Next page")
                }
            }
            .onAppear() {
                print("View with page \(page) is appearing")
            }
            .navigationTitle("Page \(page)")
            .navigationBarTitleDisplayMode(.inline)
            .navigationBarBackButtonHidden() //hide default back button
            .toolbar {
                ToolbarItem(placement: .topBarLeading) {
                    
                    //Custom back button
                    Button {
                        //Back to root
                        path.removeLast(path.count)
                    } label: {
                        HStack {
                            Image(systemName: "chevron.left")
                            Text("Back to Main")
                        }
                    }
                }
            }
        }
    }
    
    #Preview {
        PopNavigation()
    }