In SwiftUI a TabView must be the root view. You, therefore, cannot use a NavigationLink to navigate to a TabView. So say for instance I have four screens in my app.
Screen A is a TabView that holds Screen B and Screen C. Screen B is a List that has a NavigationLink to take you to a list item details (Screen D) Screen C is an information view (it's not important in the question) Screen D is a list item details screen, you must first navigate to screen b to get here. Screen D, however, has a button that should perform a network action in a ViewModel, and then take you to ScreenA upon completion.
How can Screen D navigation back two levels to the root screen (Screen A)?
An effective way to "pop" to your root view would be to utilize the isDetailLink modifier on the NavigationLink that is used for navigating.
By default, isDetailLink is true. This modifier is used for a variety of view containers, like on iPad, where the detail view would appear on the right side.
Setting isDetailLink to false means that the view will be pushed on top of the NavigationView stack, and can also be pushed off.
Along with setting isDetailLink to false on NavigationLink, pass the isActive binding to each child destination view. When you want to pop to the root view, set the value to false and it will pop everything off:
import SwiftUI
struct ScreenA: View {
@State var isActive : Bool = false
var body: some View {
NavigationView {
NavigationLink(
destination: ScreenB(rootIsActive: self.$isActive),
isActive: self.$isActive
) {
Text("ScreenA")
}
.isDetailLink(false)
.navigationBarTitle("Screen A")
}
}
}
struct ScreenB: View {
@Binding var rootIsActive : Bool
var body: some View {
NavigationLink(destination: ScreenD(shouldPopToRootView: self.$rootIsActive)) {
Text("Next screen")
}
.isDetailLink(false)
.navigationBarTitle("Screen B")
}
}
struct ScreenD: View {
@Binding var shouldPopToRootView : Bool
var body: some View {
VStack {
Text("Last Screen")
Button (action: { self.shouldPopToRootView = false } ){
Text("Pop to root")
}
}.navigationBarTitle("Screen D")
}
}
I did a trick like this, worked for me.
In SceneDelegate.swift, I modified auto generated code.
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
// Trick here.
let nav = UINavigationController(
rootViewController: UIHostingController(rootView: contentView)
)
// I embedded this host controller inside UINavigationController
// window.rootViewController = UIHostingController(rootView: contentView)
window.rootViewController = nav
self.window = window
window.makeKeyAndVisible()
}
Note: I expected embedding TabView inside NavigationView would do the same job, but did not work, it was the reason of doing this trick.
I assume, contentView is the View that you want to pop to (the one includes TabView)
Then in any view navigated from that view, you can call
extension View {
func popToRoot() {
guard let rootNav = UIApplication.shared.windows.first?.rootViewController as? UINavigationController else { return }
rootNav.popToRootViewController(animated: true)
}
}
// Assume you eventually came to this view.
struct DummyDetailView: View {
var body: some View {
Text("DetailView")
.navigationBarItems(trailing:
Button("Pop to root view") {
self.popToRoot()
}
)
}
}
// EDIT: Requested sample with a viewModel
struct DummyDetailViewWithViewModel: View {
var viewModel: SomeViewModel = SomeViewModel()
var body: some View {
Button("Complete Order!!") {
viewModel.completeOrder(success: { _ in
print("Order Completed")
self.popToRoot()
})
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With