Search code examples
swiftuiswiftui-navigationstackswiftui-navigation

SwiftUI - Undo a backwards move in a NavigationStack


When I navigate backward (like swiping the view from the left), I want to be able to swipe it back from the right to move to that previous view. Sort of like an undo stack.

Is it possible to use NavigationStack or other existing views to solve that?


Solution

  • It doesn't come for free, but you can implement this kind of functionality quite easily by saving the last navigation target and handling drag gestures. Like this:

    struct ContentView: View {
    
        @State private var navPath = [Int]()
        @State private var previousTarget = -1
    
        private func dragNavigate(back: Bool) -> some Gesture {
            DragGesture()
                .onChanged() { value in
                    let translation = value.translation
                    if abs(translation.width) > abs(translation.height) &&
                        back == (translation.width > 0) {
                        if back && navPath.count > 0 {
                            navPath.removeLast()
                        } else if !back && previousTarget >= 0 && previousTarget != navPath.last {
                            navPath.append(previousTarget)
                        }
                    }
                }
        }
    
        private func targetView(index: Int) -> some View {
            Text("View\(index + 1)")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
                .contentShape(Rectangle())
                .gesture(dragNavigate(back: true))
        }
    
        var body: some View {
            NavigationStack(path: $navPath) {
                VStack(spacing: 50) {
                    NavigationLink("View1", value: 0)
                    NavigationLink("View2", value: 1)
                    NavigationLink("View3", value: 2)
                }
                .navigationDestination(for: Int.self) { index in
                    switch index {
                    case 1: targetView(index: 1)
                    case 2: targetView(index: 2)
                    default: targetView(index: 0)
                    }
                }
                .onChange(of: navPath) { newPath in
                    if let target = newPath.last {
                        previousTarget = target
                    }
                }
            }
            .gesture(dragNavigate(back: false))
        }
    }
    

    If the navigation hierarchy extends to more than one level then you might need to save more than one previous target, perhaps as a shadow stack.