0

I have a small problem with transitions between views in my app. Here's the code that I use to handling navigation:

class NavigationStack: ObservableObject {
fileprivate private(set) var navigationType: NavigationType = .push
fileprivate private(set) var navigationTransition: NavigationTransition = .scale


private var viewStack = ViewStack() {
    didSet {
        currentView = viewStack.peek()
    }
}
@Published fileprivate var currentView: ViewElement?


func push<Element: View>(_ element: Element, withAnimation animation: NavigationTransition = .scale, withId identifier: String? = nil) {
    navigationTransition = animation
    withAnimation(navigationTransition.animation) {
        navigationType = .push
        viewStack.push(ViewElement(id: identifier == nil ? UUID().uuidString : identifier!,
                                   wrappedElement: AnyView(element)))
    }
}


func pop(to: PopDestination = .previous) {
    withAnimation(navigationTransition.animation) {
        navigationType = .pop
        switch to {
        case .root:
            viewStack.popToRoot()
        case .view(let viewId):
            viewStack.popToView(withId: viewId)
        default:
            viewStack.popToPrevious()
        }
    }
}


private struct ViewStack {
    private var views = [ViewElement]()

    func peek() -> ViewElement? {
        views.last
    }

    mutating func push(_ element: ViewElement) {
        guard indexForView(withId: element.id) == nil else {
            print("Duplicated view identifier: \"\(element.id)\". You are trying to push a view with an identifier that already exists on the navigation stack.")
            return
        }
        views.append(element)
    }

    mutating func popToPrevious() {
        _ = views.popLast()
    }

    mutating func popToView(withId identifier: String) {
        guard let viewIndex = indexForView(withId: identifier) else {
            print("Identifier \"\(identifier)\" not found. You are trying to pop to a view that doesn't exist.")
            return
        }
        views.removeLast(views.count - (viewIndex + 1))
    }

    mutating func popToRoot() {
        views.removeAll()
    }

    private func indexForView(withId identifier: String) -> Int? {
        views.firstIndex {
            $0.id == identifier
        }
    }
}

private struct ViewElement: Identifiable, Equatable {
    let id: String
    let wrappedElement: AnyView


    static func == (lhs: ViewElement, rhs: ViewElement) -> Bool {
        lhs.id == rhs.id
    }
}

struct NavigationStackView<Root>: View where Root: View {
@ObservedObject private var navViewModel: NavigationStack = NavigationStack()
@ObservedObject private var popupViewModel: PopupManager = PopupManager()
private let rootViewID = "root"
private let rootView: Root


init(@ViewBuilder rootView: () -> Root) {
    self.rootView = rootView()
}


var body: some View {
    let showRoot = navViewModel.currentView == nil
    let navigationType = navViewModel.navigationType
    let transitions = navViewModel.navigationTransition.transitions

    let showPopup = popupViewModel.openPopup
    let popup = popupViewModel.popup


    return ZStack {
        Group {
            if showRoot {
                rootView
                    .id(rootViewID)
                    .transition(navigationType == .push ? transitions.push : transitions.pop)
                    .environmentObject(navViewModel)
                    .environmentObject(popupViewModel)
            } else {
                navViewModel.currentView!.wrappedElement
                    .id(navViewModel.currentView!.id)
                    .transition(navigationType == .push ? transitions.push : transitions.pop)
                    .environmentObject(navViewModel)
                    .environmentObject(popupViewModel)
            }
        }

        if showPopup {
            popup
                .environmentObject(popupViewModel)
                .ignoresSafeArea()
        }
    }
}

The above code working in the following way: I include the NavigationStack to the first screen, and then, when I want to change screen I use @EnvironmentObject NavigationStack value's and its method push. The problem is when I want to change transition animation between screens (let's say I want to use slide instead scale), the screen that is about to leave still holding "old" transition (it's because when the currentView is changing, there is no way to access to the previous view and change its transition). The only way to squash this bug is to declare local variable @State transition: AnyTransition in the view that I want to change, and change it before I call method push:

@State private var transition: AnyTransition = .opacity

var body: some View {
    ZStack {}
        .transition(transition)
    }

func openView() {
    transition = .slide
    navigationStack.push(newView(), withAnimation: .slide)
}

But I want to implement that change to the NavigationStack(View) class. And there is my question - is there any way to get handle to the "old" view? I would be very grateful for any tips!

John Clark
  • 11
  • 1
  • This should be helpful https://stackoverflow.com/a/59087574/12299030. – Asperi Nov 18 '20 at 11:53
  • Thanks, it's nice but the problem with this solution is that it doesn't support many transition methods. I will try to visualize my problem: I have three transition animation methods in my app that could be launched from the view A - scale, vertical slide and horizontal slide. The problem is the view that should be removed, uses the latest transition (not the new one) and the view that should be presented uses the right transition. Look at this: https://vimeo.com/480737563 – John Clark Nov 18 '20 at 12:30

0 Answers0