3

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)?

Adrian Le Roy Devezin
  • 672
  • 1
  • 13
  • 41
  • If ScreenA is TabView{ScreenB|ScreenC} and ScreenB > ScreenD then I don't see two level navigation, did I miss anything? The scenario is not clear. – Asperi May 03 '20 at 06:04

3 Answers3

3

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")
    }
}
  • How would this work with the MVVM setup though? Each screen in swift should have a ViewModel. So say once your at screen D, you press a button that performs a network call in the viewmodel. Then when that network call completes and is successful, is when you should pop to the root view – Adrian Le Roy Devezin May 01 '20 at 20:29
  • Good but when ScreenB showed, I would like TabView navigation bar must be hide - how to do it? – Igor Cova May 13 '20 at 20:54
  • @IgorCova for hiding the tab bar you can place TabView inside NavigationView. 1.NavigationView -> Link -> TabView screen -> Every Tab can contain NavigationLink, not View. – Oleksii Radetskyi Dec 02 '20 at 11:45
2

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()
            })
        }
    }
}
Enes Karaosman
  • 1,889
  • 20
  • 27
  • How would this work with the MVVM setup though? Each screen in swift should have a ViewModel. So say once your at screen D, you press a button that performs a network call in the viewmodel. Then when that network call completes and is successful, is when you should pop to the root view – Adrian Le Roy Devezin May 01 '20 at 20:29
  • Does not matter where you call this. For ex: you tapped a button & made network call. You can give a success callback back to the view you performed operation. so you may trigger that method there. – Enes Karaosman May 03 '20 at 09:40
  • ;( You can hide it if you want. But if you want to push to a detail view, you gotta be in the navigation view, this is how it works – Enes Karaosman May 04 '20 at 02:47
  • if you want to get rid of the navigation bar, I recommend [this solution](https://stackoverflow.com/questions/57517803/how-to-remove-the-default-navigation-bar-space-in-swiftui-navigiationview) – Joshua Martinez May 06 '20 at 04:54
2

I have resolved this with self.presentationMode.wrappedValue.dismiss(). This is the method that is called to bring back to View of Navigation Root. Here, TestView is ScreenA, ItemList is ScreenB, InfoView is ScreenC and ItemDetails is ScreenD.

import SwiftUI

struct TestView: View {
    @State private var currentTab: Tab = .list
    var body: some View {
        TabView(selection: $currentTab){
            ItemList()
                .tabItem{
                    Text("List")
            }
            .tag(Tab.list)
            .navigationBarHidden(false)
            InfoView()
                .tabItem{
                    Text("Info")
            }
            .tag(Tab.info)
            .navigationBarHidden(true)
        }
    }
}

struct ItemList: View {
    var body: some View {
        VStack{
            NavigationView{
                List {
                    NavigationLink(destination: ItemDetails()){
                        Text("Item")
                    }
                    NavigationLink(destination: ItemDetails()){
                        Text("Item")
                    }
                    NavigationLink(destination: ItemDetails()){
                        Text("Item")
                    }
                    NavigationLink(destination: ItemDetails()){
                        Text("Item")
                    }
                }.navigationBarTitle("Item List")
            }
        }
    }
}

struct InfoView: View {
    var body: some View {
        Text("This is information view")
    }
}

struct ItemDetails: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    @State private var loading = false
    var body: some View {
        ZStack {
            Text("Connecting...")
                .font(.title)
                .offset(y: -150)
                .pulse(loading: self.loading)
            VStack {
                Text("This is Item Details")
                Button("Connect"){
                    self.loading = true
                    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
                        self.loading = false
                        self.presentationMode.wrappedValue.dismiss()
                    }
                }.padding()
            }
        }
    }
}

enum Tab {
    case list, info
}

extension View {
    func pulse(loading: Bool) -> some View {
        self
            .opacity(loading ? 1 : 0)
            .animation(
                Animation.easeInOut(duration: 0.5)
                    .repeatForever(autoreverses: true)
        )

    }
}
NikzJon
  • 912
  • 7
  • 25