Search code examples
iosswiftswiftuiswiftui-listswiftui-navigationlink

Dismissing multiple views within navigation stack


I have stacked multiple view via NavigationLink within a list. My app code is complex, so I have tried to simplify it as much as I could.

Let's say I have a button "Click Me" in one of the main view's (to keep things simple, I have kept it in ContentView in my sample code) which when clicked opens a sheet. This sheet has a navigation stack and navigates to 3 different views.

Last view (named as "ThirdListView") has 2 buttons - Done and Reset. When Done button is tapped, I want to close all the views and dismiss the sheet and get back to Main view (Content View in this case). When Reset button is tapped, I want to get to the first view in the sheet.

Here is how the hierarchy of my code is:

Content View -> Opens a sheet called as MediatorView -> Opens FirstListView within same sheet -> Opens SecondListView within same sheet -> Opens ThirdListView within same sheet.

Here is what I am trying to achieve:

Tap Reset on ThirdListView, app should navigate back to MediatorView
Tap Done on ThirdListView, app should navigate back to ContentView 

Code:

import SwiftUI
import Foundation

struct ContentView: View {
    @State var openMediatorView: Bool = false
    var body: some View {
        VStack {
            Button {
                self.openMediatorView.toggle()
            } label: {
                Text("Click Me").font(.body.bold())
            }
        }
        .sheet(isPresented: self.$openMediatorView) {
            MediatorView()
        }
    }
}

struct MediatorView: View {
    let firstList: [String] = ["One", "Two", "Three", "Four", "Five"]
    let secondList: [String] = ["One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten"]
    let thirdList: [String] = ["One", "Two", "Three", "Four", "Five", "Six", "Seven"]
    
    @State var showFirstRow: Bool = false

    var body: some View {
        NavigationStack {
            Group {
                if firstList.count > 1 {
                    FirstListView(isHidden: .constant(true), shortList: firstList, longList: secondList, thirdList: thirdList)
                } else {
                    SecondListView(isHidden: .constant(true), longList: secondList, thirdList: thirdList).navigationBarBackButtonHidden()
                }
            }
            .interactiveDismissDisabled()
        }
    }
}

struct FirstListView: View {
    init(isHidden: Binding<Bool>, shortList: [String], longList: [String], thirdList: [String]) {
        self.isHidden = isHidden
        self.shortList = shortList
        self.longList = longList
        self.thirdList = thirdList
    }
    
    var isHidden: Binding<Bool>
    var shortList: [String]
    var longList: [String]
    var thirdList: [String]
    
    var body: some View {
        ScrollViewReader { proxy in
            VStack {
                List(shortList, id: \.hashValue) { value in
                    NavigationLink(destination: {
                        SecondListView(isHidden: .constant(true), longList: longList, thirdList: thirdList)
                    },
                    label: {
                        HStack {
                            Text(value)
                            Spacer()
                        }
                        .contentShape(Rectangle())
                    })
                }
                .listStyle(.plain)
                .contentShape(Rectangle())
            }
        }
        .navigationTitle("First List")
        .interactiveDismissDisabled()
    }
}

struct SecondListView: View {
    init(isHidden: Binding<Bool>, longList: [String], thirdList: [String]) {
        self.isHidden = isHidden
        self.longList = longList
        self.thirdList = thirdList
    }
    
    var isHidden: Binding<Bool>
    var longList: [String]
    var thirdList: [String]
    
    var body: some View {
        ScrollViewReader { proxy in
            VStack {
                List(longList, id: \.hashValue) { value in
                    NavigationLink(destination: {
                        ThirdListView(isHidden: .constant(true), thirdList: thirdList)
                    },
                    label: {
                        HStack {
                            Text(value)
                            Spacer()
                        }
                        .contentShape(Rectangle())
                    })
                }
                .listStyle(.plain)
                .contentShape(Rectangle())
            }
        }
        .navigationTitle("Second List")
        .interactiveDismissDisabled()
    }
}

struct ThirdListView: View {
    @Environment(\.presentationMode) private var presentationMode
    
    init(isHidden: Binding<Bool>, thirdList: [String]) {
        self.isHidden = isHidden
        self.thirdList = thirdList
    }
    
    var isHidden: Binding<Bool>
    var thirdList: [String]
    
    var body: some View {
        ScrollViewReader { proxy in
            VStack {
                List(thirdList, id: \.hashValue) { value in
                    HStack {
                        Text(value)
                        Spacer()
                    }
                    .contentShape(Rectangle())
                }
                .listStyle(.plain)
                .contentShape(Rectangle())
                
                // TODO: Tapping this should navigate this view back to Mediator View i.e. first view in the sheet.
                Button {
                    isHidden.wrappedValue = false
                    presentationMode.wrappedValue.dismiss()
                } label: {
                    Text("Reset to Mediator View").font(.body.bold())
                }
            }
        }
        .interactiveDismissDisabled()
        .navigationTitle("Third List")
        .toolbar {
            ToolbarItem(placement: .topBarTrailing) {
                // TODO: Tapping this should navigate this view back to Content View i.e. close the sheet completely.
                Button {
                    isHidden.wrappedValue = false
                    presentationMode.wrappedValue.dismiss()
                } label: {
                    Text("Done").font(.body.bold())
                }
            }
        }
    }
}

Solution

  • You can use NavigationPath inside the MediatorView. By using NavigationPath you can build the navigation hierarchy, and also you can modify the hierarchy as you want. You can navigate to other views by appending to navigationPath and also get back by removing the last element of it, so you can go back to root by making the navigationPath empty.

    here is the NavigationPath version of NavigationStack:

    struct FirstListItem: Hashable {
        init(_ item: String) {
            self.item = item
        }
        let item: String
    }
    struct SecondListItem: Hashable {
        init(_ item: String) {
            self.item = item
        }
        let item: String
    }
    struct ThirdListItem: Hashable {
        init(_ item: String) {
            self.item = item
        }
        let item: String
    }
    
    struct ContentView: View {
        @State var openMediatorView: Bool = false
        var body: some View {
            VStack {
                Button {
                    self.openMediatorView.toggle()
                } label: {
                    Text("Click Me").font(.body.bold())
                }
            }
            .sheet(isPresented: self.$openMediatorView) {
                MediatorView(openMediatorView: $openMediatorView)
            }
        }
    }
    
    struct MediatorView: View {
        let firstList: [FirstListItem] = [
            FirstListItem("One"),
            FirstListItem("Two"),
            FirstListItem("Three"),
            FirstListItem("Four"),
            FirstListItem("Five")
        ]
        let secondList: [SecondListItem] = [
            SecondListItem("One"),
            SecondListItem("Two"),
            SecondListItem("Three"),
            SecondListItem("Four"),
            SecondListItem("Five"),
            SecondListItem("Six"),
            SecondListItem("Seven"),
            SecondListItem("Eight"),
            SecondListItem("Nine"),
            SecondListItem("Ten")
        ]
        let thirdList: [ThirdListItem] = [
            ThirdListItem("One"),
            ThirdListItem("Two"),
            ThirdListItem("Three"),
            ThirdListItem("Four"),
            ThirdListItem("Five"),
            ThirdListItem("Six"),
            ThirdListItem("Seven")
        ]
    
        @Binding var openMediatorView: Bool
        @State private var navigationPath = NavigationPath()
    
        var body: some View {
            NavigationStack(path: $navigationPath) {
                Group {
                    if firstList.count > 1 {
                        FirstListView(shortList: firstList,
                                      longList: secondList,
                                      thirdList: thirdList,
                                      navigationPath: $navigationPath)
                    } else {
                        SecondListView(longList: secondList,
                                       thirdList: thirdList,
                                       navigationPath: $navigationPath)
                        .navigationBarBackButtonHidden()
                    }
                }
                .interactiveDismissDisabled()
                .navigationDestination(for: FirstListItem.self) { selection in
                    SecondListView(longList: secondList,
                                   thirdList: thirdList,
                                   navigationPath: $navigationPath)
                }
                .navigationDestination(for: SecondListItem.self) { selection in
                    ThirdListView(isHidden: $openMediatorView,
                                  thirdList: thirdList,
                                  navigationPath: $navigationPath)
                }
            }
        }
    }
    
    struct FirstListView: View {
        var shortList: [FirstListItem]
        var longList: [SecondListItem]
        var thirdList: [ThirdListItem]
        @Binding var navigationPath: NavigationPath
    
        var body: some View {
            ScrollViewReader { proxy in
                VStack {
                    List(shortList, id: \.self) { value in
                        HStack {
                            Button {
                                navigationPath.append(value)
                            } label: {
                                Text(value.item)
                            }
                            Spacer()
                        }
                        .contentShape(Rectangle())
                    }
                    .listStyle(.plain)
                    .contentShape(Rectangle())
                }
            }
            .navigationTitle("First List")
            .interactiveDismissDisabled()
        }
    }
    
    struct SecondListView: View {
        var longList: [SecondListItem]
        var thirdList: [ThirdListItem]
        @Binding var navigationPath: NavigationPath
    
        var body: some View {
            ScrollViewReader { proxy in
                VStack {
                    List(longList, id: \.hashValue) { value in
                        HStack {
                            Button {
                                navigationPath.append(value)
                            } label: {
                                Text(value.item)
                            }
                            Spacer()
                        }
                        .contentShape(Rectangle())
                    }
                    .listStyle(.plain)
                    .contentShape(Rectangle())
                }
            }
            .navigationTitle("Second List")
            .interactiveDismissDisabled()
        }
    }
    
    struct ThirdListView: View {
        @Binding var isHidden: Bool
        var thirdList: [ThirdListItem]
        @Binding var navigationPath: NavigationPath
    
        var body: some View {
            ScrollViewReader { proxy in
                VStack {
                    List(thirdList, id: \.hashValue) { value in
                        HStack {
                            Text(value.item)
                            Spacer()
                        }
                        .contentShape(Rectangle())
                    }
                    .listStyle(.plain)
                    .contentShape(Rectangle())
    
                    // TODO: Tapping this should navigate this view back to Mediator View i.e. first view in the sheet.
                    Button {
                        navigationPath = NavigationPath()
                    } label: {
                        Text("Reset to Mediator View").font(.body.bold())
                    }
                }
            }
            .interactiveDismissDisabled()
            .navigationTitle("Third List")
            .toolbar {
                ToolbarItem(placement: .topBarTrailing) {
                    // TODO: Tapping this should navigate this view back to Content View i.e. close the sheet completely.
                    Button {
                        isHidden = false
                    } label: {
                        Text("Done").font(.body.bold())
                    }
                }
            }
        }
    }
    
    #Preview {
        ContentView()
    }
    

    Read more about NavigationPath