0

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?

roydbt
  • 91
  • 1
  • 8

1 Answers1

1

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.

Benzy Neez
  • 1,546
  • 2
  • 3
  • 10
  • So if I want it to feel "native", I have to draw both the `previousTarget` view and the back view behind the current view and play with the offset of the current one? – roydbt Jul 14 '23 at 19:48
  • ? Don't think so. The ```NavigationStack``` is bound to the array ```navPath```. When an index value (used to identify a navigation target) is added to the path it navigates to that target view and when a value is popped from the stack it navigates back. Did you try the example implementation I provided? – Benzy Neez Jul 14 '23 at 20:03
  • What I mean is that a small and unreleased swipe doesn't automatically navigate the views, you can control the flow. I don't really know how to explain it in text. – roydbt Jul 14 '23 at 20:19
  • 1
    OK, so you don't want the navigation change to happen until the drag is released? Until it is released, you want a glimpe of the view it will go to, maybe with the option of not performing the navigation change after all if the drag is only small? This is possible but more complicated. It means changing the drag gesture completely and, as you say, providing some way of showing a preview. I would suggest, this would be best explored as a new question. – Benzy Neez Jul 14 '23 at 20:46