skip to Main Content

I’m using a NavigationStack and I’m dealing with the path programmatically, so I’m always the one appending and removing entries from it to control navigation.

Any time I add or remove views from the path, I get a nice transition that makes sense based on what happened in the path. But things start to get interesting when I remove and add views at the same time:

  • If after I add and remove views the path is larger than before, I get a transition as if I’m navigating forward, which makes a lot of sense.
  • If after I add and remove views the path is smaller than before, I get a transition as if I’m navigating backwards. This isn’t great because sometimes the user is actually navigating forward to a new screen, but they may have done something that caused all the screens behind to become irrelevant (e.g. after logging in), so sometimes it’s weird to get a backward transition. It would be much better if it understood the cases where the tip of the path is a new screen, thus it should be a normal forward transition regardless to what happened to the other screens in the path.
  • And finally, if the path remains with the same size after I add and remove views from it, then I get no transition at all. Even though the path changes, it’s like the implementation is like “well, it still has the same size, so it must be the same” even though that’s not the case.

Is there anything I can do to have more control over the transitions?

2

Answers


  1. You have little control over the transitions. This question was about disabling the transitions, and even for that you need to rely on UIKit APIs.

    That said, once we know how to disable transitions, we can force a "forward" transition. If you want to replace the top of the stack with aNewValue, or if you want to remove the top 2 items, then add aNewValue, you can force a forward transition by doing the following steps:

    1. append aNewValue to the stack
    2. disable animations
    3. remove the last n things in the path
    4. add aNewValue back
    5. enable animations again

    e.g.

    // this changes the top of the navigation path to "Something Else"
    path.append("Something Else")
    DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1)) {
        UIView.setAnimationsEnabled(false)
        path.removeLast(2)
        path.append("Something Else")
        DispatchQueue.main.async {
            UIView.setAnimationsEnabled(true)
        }
    }
    

    I can’t think of how you would force a "backward" transition with a type erased NavigationPath, but if your navigation path is a typed array, you can

    1. disable animations
    2. insert the new element as the penultimate item in the path (this doesn’t exist on NavigationPaths unfortunately)
    3. enable animations
    4. remove the last element from the path
    UIView.setAnimationsEnabled(false)
    path.insert("Something Else", at: path.count - 1)
    DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1)) {
        UIView.setAnimationsEnabled(true)
        path.removeLast()
    }
    
    Login or Signup to reply.
  2. You’re talking about cyclic navigation. If you were just to keep appending to the path then it would work like a browser and Back would step back along exactly the same path. This would also resolve your transition issue. But if you want to keep the path optimized then one solution is to perform your own transitions (and in fact, your own navigation) once you are in the realm of the revolving views.

    The following example is based on the solution I provided to SwiftUI bi-directional move transition moving the wrong way in certain cases. The views have an implicit hierarchy, so a lower view always slides in from the right and a higher view always slides in from the left. The Back button simply returns to the initial menu.

    enum NavTarget: Int, Comparable {
        case first = 1
        case second = 2
        case third = 3
    
        static func < (lhs: NavTarget, rhs: NavTarget) -> Bool {
            lhs.rawValue < rhs.rawValue
        }
    }
    
    struct ViewPair<Target: Comparable, PrimaryContent: View, SecondaryContent: View>: View {
        let primaryView: Target
        let selectedView: Target
        let primaryContent: PrimaryContent
        let secondaryContent: SecondaryContent
    
        var body: some View {
            ZStack {
                if selectedView <= primaryView {
                    primaryContent
                        .transition(.move(edge: .leading))
                } else {
                    secondaryContent
                        .transition(.move(edge: .trailing))
                }
            }
        }
    }
    
    struct RevolvingViews: View {
        @State var target: NavTarget
    
        private var view1: some View {
            VStack(spacing: 20) {
                Text("This is view 1")
                Button("View 2") { target = .second }
                Button("View 3") { target = .third }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    
        private var view2: some View {
            VStack(spacing: 20) {
                Button("View 1") { target = .first }
                Text("This is view 2")
                Button("View 3") { target = .third }
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    
        private var view3: some View {
            VStack(spacing: 20) {
                Button("View 1") { target = .first }
                Button("View 2") { target = .second }
                Text("This is view 3")
            }
            .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    
        var body: some View {
            ViewPair(
                primaryView: NavTarget.first,
                selectedView: target,
                primaryContent: view1,
                secondaryContent: ViewPair(
                    primaryView: NavTarget.second,
                    selectedView: target,
                    primaryContent: view2,
                    secondaryContent: view3
                )
            )
            .animation(.easeInOut, value: target)
        }
    }
    
    struct ContentView: View {
    
        @State private var path = [NavTarget]()
        @State private var secondaryTarget = NavTarget.first
    
        var body: some View {
            NavigationStack(path: $path) {
                VStack(spacing: 20) {
                    NavigationLink("View 1", value: NavTarget.first)
                    NavigationLink("View 2", value: NavTarget.second)
                    NavigationLink("View 3", value: NavTarget.third)
                }
                .navigationDestination(for: NavTarget.self) { target in
                    RevolvingViews(target: target)
                }
            }
        }
    }
    

    RevolvingViews

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search