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

2

7

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.

Templeton answered 9/4, 2021 at 2:31 Comment(0)
P
5

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() }
    }
}
Paraphrast answered 9/4, 2021 at 5:53 Comment(0)
E
1

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.

Endure answered 9/4, 2021 at 3:21 Comment(4)
hey mate, thanks for your answer, I forgot to mention that i'm using @Environment(\.presentationMode) var presentationMode and presentationMode.wrappedValue.dismiss() in X action, but it only dismiss to previous page while I want to go back to my root view.Templeton
@AldoSugiarto i have edited the answer to include the solution for navigation all the way back.. it is a little enveloped so i posted a link to my wife blog post on this.Endure
From what i read on the blog and tried the example project, SceneStorage can persist the state of the view even when we close and re-run the project. I just wondering, if the SceneStorage can tag me when I select the second tab, but when go deeper and deeper in the views, how is it going to pop all the views and back to the second tab?Templeton
you can access the same SceneStorage key on that nested view then change the value, the top level will detect the change and response changing your navigation. You can have the same value many many times throughout your app (i use the excessively in my apps to do lots of stuff to navigation, like select an item after creating it)Endure

© 2022 - 2024 — McMap. All rights reserved.