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())
}
}
}
}
}
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()
}