iosswiftswiftuiswiftui-navigationlinkswiftui-navigationstack

SwiftUI: How to Navigate from Screen C Back to Screen A (Skipping Screen B) with Back Swipe Gesture?


I'm building a SwiftUI app where I have three screens (A, B, and C) navigated sequentially. The flow looks like this:

Screen A -> Screen B -> Screen C

I want to achieve the following behavior:

When the user swipes back from Screen C, they should be taken directly to Screen A, skipping Screen B.(iOS native back swipe)

In Screen B, instead of using navigationPath.append(viewC), I tried setting the path like this:

navigationPath = [.viewA, viewC]

This results in no animation at all when navigating to Screen C.

i also try this

{
navigationPath.removeLast
navigationPath.append(viewC)
}

This results in no animation at all when navigating to Screen C.

In onAppear of Screen C, I tried resetting the path like this:

navigationPath = [.viewA, viewC]

Here, I do get the animation when transitioning to Screen C, when onAppear works i seen transition back animation (from viewC to viewC).

Is there a better way to achieve this behavior where swiping back from Screen C directly returns to Screen A without any unintended animations? Any advice would be greatly appreciated!

Code sample:

import SwiftUI

enum Route {
    case viewA, viewB, viewC
}

struct ContentView: View {
    @State private var path: [Route] = []

    var body: some View {
        NavigationStack(path: $path) {
            Text("This is root view")
            Button {
                path.append(.viewA)
            } label: {
                Text("Go to screen A")
            }
                .navigationDestination(for: Route.self) { route in
                    switch route {
                    case .viewA:
                        AView(path: $path)
                    case .viewB:
                        BView(path: $path)
                    case .viewC:
                        CView(path: $path)
                    }
                }
            
        }
    }
}

struct AView: View {
    @Binding var path: [Route]

    var body: some View {
        VStack {
            Text("This is Screen A")
            Button("Go to Screen B") {
                path.append(.viewB)
            }
        }
    }
}

struct BView: View {
    @Binding var path: [Route]

    var body: some View {
        VStack {
            Text("This is Screen B")
            Button("Go to Screen C") {
                path = [.viewA, .viewC]
            }
        }
    }
}

struct CView: View {
    @Binding var path: [Route]

    var body: some View {
        VStack {
            Text("This is Screen C")
        }
    }
}

#Preview {
    ContentView()
}

Solution

  • Assuming that what you're looking for is to navigate to screen C with an animation, but without having a double animation upon arriving on screen C, I believe the following should do the trick:

    struct BView: View {
        @Binding var path: [Route]
        
        var body: some View {
            VStack {
                Text("This is Screen B")
                Button("Go to Screen C") {
                        path.append(.viewC)
                }
            }
            .onDisappear { // <-- Here, add .onDisappear modifier to screen B
                var transaction = Transaction()
                transaction.disablesAnimations = true
                withTransaction(transaction) { // <-- Here, wrap path mutation in withTransaction, where transaction has disablesAnimations set to true
                    path = [.viewA, .viewC]
                }
            }
        }
    }
    

    You were on the right path, trying to alter the path after screen B disappeared, but that caused screen C to be animated twice: the first time when it naturally appeared, and the second time when the path was reset as part of the .onDisappear action of screen B.

    Apparently, since iOS 16.2 you can disable animations on the NavigationStack push and pop, as shown above. I found that answer here.

    Although there still is a double navigation happening as described above, it won't be apparent to the user. You can see it in action if you give each screen a title, and then notice the back button title changing to the screen A title upon arriving on screen C (and after screen B disappeared).

    So this may not be ideal if you use custom navigation titles for each screen.

    An alternative to this approach is to hide the back button in screen C using .navigationBarBackButtonHidden(true) and set a custom back button in the toolbar with the same logic as that of the button in screen C (something like path = removeLast(2)), but that will cause the loss of the back swipe gesture. The gesture can supposedly be restored, but I haven't tried it myself.