swiftswiftui

How can I pop to the Root view using SwiftUI?


Finally now with Beta 5 we can programmatically pop to a parent View. However, there are several places in my app where a view has a "Save" button that concludes a several step process and returns to the beginning. In UIKit, I use popToRootViewController(), but I have been unable to figure out a way to do the same in SwiftUI.

Below is a simple example of the pattern I'm trying to achieve.

How can I do it?

import SwiftUI

struct DetailViewB: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    var body: some View {
        VStack {
            Text("This is Detail View B.")

            Button(action: { self.presentationMode.value.dismiss() } )
            { Text("Pop to Detail View A.") }

            Button(action: { /* How to do equivalent to popToRootViewController() here?? */ } )
            { Text("Pop two levels to Master View.") }

        }
    }
}

struct DetailViewA: View {
    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
    var body: some View {
        VStack {
            Text("This is Detail View A.")

            NavigationLink(destination: DetailViewB() )
            { Text("Push to Detail View B.") }

            Button(action: { self.presentationMode.value.dismiss() } )
            { Text("Pop one level to Master.") }
        }
    }
}

struct MasterView: View {
    var body: some View {
        VStack {
            Text("This is Master View.")

            NavigationLink(destination: DetailViewA() )
            { Text("Push to Detail View A.") }
        }
    }
}

struct ContentView: View {
    var body: some View {
        NavigationView {
            MasterView()
        }
    }
}

Solution

  • iOS 16 Update: NavigationPath was added to make this easier. Use with the new NavigationStack that also fixes a lot of bugs.

    Setting the view modifier isDetailLink to false on a NavigationLink is the key to getting pop-to-root to work. isDetailLink is true by default and is adaptive to the containing View. On iPad landscape for example, a Split view is separated and isDetailLink ensures the destination view will be shown on the right-hand side. Setting isDetailLink to false consequently means that the destination view will always be pushed onto the navigation stack; thus can always be popped off.

    Along with setting isDetailLink to false on NavigationLink, pass the isActive binding to each subsequent destination view. At last when you want to pop to the root view, set the value to false and it will automatically pop everything off:

    import SwiftUI
    
    struct ContentView: View {
        @State var isActive : Bool = false
    
        var body: some View {
            NavigationView {
                NavigationLink(
                    destination: ContentView2(rootIsActive: self.$isActive),
                    isActive: self.$isActive
                ) {
                    Text("Hello, World!")
                }
                .isDetailLink(false)
                .navigationBarTitle("Root")
            }
        }
    }
    
    struct ContentView2: View {
        @Binding var rootIsActive : Bool
    
        var body: some View {
            NavigationLink(destination: ContentView3(shouldPopToRootView: self.$rootIsActive)) {
                Text("Hello, World #2!")
            }
            .isDetailLink(false)
            .navigationBarTitle("Two")
        }
    }
    
    struct ContentView3: View {
        @Binding var shouldPopToRootView : Bool
    
        var body: some View {
            VStack {
                Text("Hello, World #3!")
                Button (action: { self.shouldPopToRootView = false } ){
                    Text("Pop to root")
                }
            }.navigationBarTitle("Three")
        }
    }
    
    struct ContentView_Previews: PreviewProvider {
        static var previews: some View {
            ContentView()
        }
    }
    

    Screen capture