How to dismiss a presenting view to the root view of tab view in SwiftUI?

Viewed 2093

I'm using TabView on my home page. Let's just say I have 4 tabs. On second tab, i can go to another view using NavigationLink and I go to another 2 views using NavigationLink. Then on the latest view, there is a button to present a view and i use .fullScreenCover (since I want to present it full screen).

In the presenting view, I add an X mark on the left side of the navigationBarItems to dismiss. I use @Environment(\.presentationMode) var presentationMode and presentationMode.wrappedValue.dismiss() to dismiss. But it only dismiss the presenting view to the previous view, while actually I want to dismiss it to the root of my view which is the 2nd tab of my TabView.

Is there a way to do this? Because I have looked up to some articles and nothing relevant especially in TabView context.

I also have a question tho:

  1. Is it a right approach to use .fullScreenCover? Or is there another possible solution for example presenting a modal with full screen style (if there's any cause i'm not sure either).

Any suggestions will be very appreciated, thankyou in advance.

2 Answers

The presentationMode is one-level effect value, ie changing it you close one currently presented screen.

Thus to close many presented screens you have to implement this programmatically, like in demo below.

The possible approach is to use custom EnvironmentKey to pass it down view hierarchy w/o tight coupling of every level view (like with binding) and inject/call only at that level where needed.

Demo tested with Xcode 12.4 / iOS 14.4

demo

struct ContentView: View {
    var body: some View {
        TabView {
            Text("Tab1")
                .tabItem { Image(systemName: "1.square") }
            Tab2RootView()
                .tabItem { Image(systemName: "2.square") }
        }
    }
}

struct Tab2RootView: View {
    @State var toRoot = false
    var body: some View {
        NavigationView {
            Tab2NoteView(level: 0)
                .id(toRoot)          // << reset to root !!
        }
        .environment(\.rewind, $toRoot)        // << inject here !!
    }
}

struct Tab2NoteView: View {
    @Environment(\.rewind) var rewind
    let level: Int

    @State private var showFullScreen = false
    var body: some View {
        VStack {
            Text(level == 0 ? "ROOT" : "Level \(level)")
            NavigationLink("Go Next", destination: Tab2NoteView(level: level + 1))
            Divider()
            Button("Full Screen") { showFullScreen.toggle() }
                .fullScreenCover(isPresented: $showFullScreen,
                                        onDismiss: { rewind.wrappedValue.toggle() }) {
                    Tab2FullScreenView()
                }
        }
    }
}

struct RewindKey: EnvironmentKey {
    static let defaultValue: Binding<Bool> = .constant(false)
}

extension EnvironmentValues {
    var rewind: Binding<Bool> {
        get { self[RewindKey.self] }
        set { self[RewindKey.self] = newValue }
    }
}

struct Tab2FullScreenView: View {
    @Environment(\.presentationMode) var mode

    var body: some View {
        Button("Close") { mode.wrappedValue.dismiss() }
    }
}

You have 2 options:

  1. With .fullScreenCover you will have a binding that results in it being presented you can pass this binding through to the content and when the user taps on x set to to false

  2. You can use the @Environment(\.presentationMode) var presentationMode then call presentationMode.wrappedValue.dismiss() in your button body.

Edit:

If you want to unwind all the way you should make the TabView be binding based. I like to use SceneStorage for this take a look at this post then you can access this SceneStorage value anywhere in your app to respond to it but also to update and change the navigation (this also has the benefit of providing you proper state restoration!)

If you make your TabView in this way:

struct ContentView: View {
    
    @SceneStorage("selectedTab") var selectedTab: Tab = .car
    
    var body: some View {
        TabView(selection: $selectedTab) {
            CarTrips()
                .tabItem {
                    Image(systemName: "car")
                    Text("Car Trips")
                }.tag(Tab.car)
            TramTrips()
                .tabItem {
                    Image(systemName: "tram.fill")
                    Text("Tram Trips")
                }.tag(Tab.tram)
            AirplaneTrips()
                .tabItem {
                    Image(systemName: "airplane")
                    Text("Airplane Trips")
                }.tag(Tab.airplaine)
        }
    }
    

}

enum Tab: String {
    case car
    case tram
    case airplaine
}

Then deep within your app in the place you want to change the navigation you can create a button view.

struct ViewCarButton: View {
    @SceneStorage("selectedTab") var selectedTab: Tab = .car

    var body: some View {
        Button("A Button") {
            selectedTab = .car
        }
    }

}

This will forced the selected tab to be the car tab.

if instead of this you do not want to change tab but rather change what the navigation view is navigated to you can use the same concept for that, NavigationLink that's a binding if this binding is created using a @SceneStorage then in your ViewCarButton you can make changes to it that will change the navigation state.

Related