Due to the horrible memory leaks when using NavigationPath() (initializing StateObjects numerous times, not releasing memory when views are removed from the stack, etc.), I am forced to use .navigationDestination(isPresented: $blah, destination { MyView() }) which is fine. I have it all working the way I need it to, except for one major issue.
Just to expand a bit, even if I don't use NavigationPath() and instead use separate arrays with a NavigationStack(path: $path) binding, it won't work because I would be forced to use switch cases with every view possible in a single location. There are many views where the variables need to be passed to the next view but have not yet been created or written yet. I need to be able to programmatically via a boolean, navigate wherever I need to go on a per page basis.
I have a bunch of very deep links throughout the app and need the ability to return to the root of where the deep links began (note that this is not the actual root view).
If I'm only a single level deep, I can simply set the isPresented bool to false and it pops perfectly. However, anymore than a single level and it does nothing.
Here's an example of what I mean (imagine that you are already one level deep into the nav stack):
class PopToHome: ObservableObject {
@Published var navToTest1ModRoot: Bool = false
}
class ViewRouter: ObservableObject {
@Published var test1Mod: Test1Module = .current
}
enum Test1Module {
case current
case previous
}
struct Test_1_Module_HomeView: View {
@EnvironmentObject var viewRouter: ViewRouter
var body: some View {
switch viewRouter.test1Mod {
case .current:
Test_1_Module_CurrentView()
.navigationBarHidden(true)
case .previous:
Test_1_Module_PreviousView()
.navigationBarHidden(true)
}
}
}
struct Test_1_ModuleNavView: View {
@EnvironmentObject var popToHome: PopToHome
@EnvironmentObject var viewRouter: ViewRouter
@State private var test1AddNav: Bool = false
var body: some View {
VStack(spacing: 0) {
EmptyView()
}
.navigationDestination(
isPresented: $test1AddNav,
destination: {
Test_1_Module_AddView()
}
)
.onReceive(self.popToHome.$navToTest1ModRoot) { navToTest1ModRoot in
if navToTest1ModRoot {
test1AddNav = false
self.popToHome.navToTest1ModRoot = false
}
}
VStack {
HStack {
Image(systemName: "folder.circle")
.onTapGesture {
viewRouter.test1Mod = .current
}
Image(systemName: "plus.circle")
.onTapGesture {
test1AddNav = true
}
Image(systemName: "bolt.slash.circle")
.onTapGesture {
viewRouter.test1Mod = .current
}
}
}
}
}
struct Test_1_Module_AddView: View {
@EnvironmentObject var popToHome: PopToHome
var body: some View {
VStack {
Button {
self.popToHome.navToTest1ModRoot = true
} label: {
Text("Tab View")
}
}
}
}
In NavigationView I was able to write viewRouter.test1Mod = .current and all the views would pop off the stack and return me to the current tab. That no longer works with NavigationStack so I'm trying to figure out how to achieve the same result.
After almost a year of trying to figure this out, and even involving Apple's TSI engineers, no one had an answer for me, but I finally solved it, and it's actually very easy, so I thought I'd share.
Reference the code from my initial post if need be because it's pretty close. Remove popToHome as it's no longer needed.
Do not use NavigationPath(). Instead, create a single @Published variable in the viewRouter class called path, making it a String array like this:
@Published var path: [String] = []
Use .navigationDestination(isPresented) just as I have shown in my original code for all single level navigation. For all navigation more than a single level deep, you will append to the path variable, and create a single
.navigationDestination(for: String.self) { navigation in
if navigation == "MyMultiPageForm" {
My_MultiPage_Form()
} else if navitation == "MyNextMultiPageForm" {
My_Next_MultiPage_Form()
}
}
within the NavigationStack enclosure on the main page where you define NavigationStack. NavigationStack should be defined as
NavigationStack(path: $viewRouter.path) {}
The final thing you should do is create two helper functions inside the viewRouter class as follows:
func popToLogin() { path.removeAll() }
func popToHome() { path.removeLast() }
Now, when you want to pop the stack all the way back to the main login page of your app, you simply call viewRouter.popToLogin(), and when you want to pop back to the home page of your multi-page form, you use popToHome().
Doing it this way allows you to use StateObjects to initialize your view models without the memory issues of NavigationPath(), and it will initialize and deinitialize your views much like NavigationView() did when doing everything in a programmatic manner.
Hope this helps!