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!