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