I'm currently using NavigationView
to layout my App for iPhone and iPad depending on the horizontalSizeClass
. Since the functions I'm using for NavigationView
and NavigationLink
are deprecated I wonder if one can achieve something similar with the latest APIs.
Here is the simplified version.
ContentView
struct ContentView: View {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
var body: some View {
if horizontalSizeClass == .compact {
iPhoneEntry()
} else {
iPadEntry()
}
}
}
AppItems
indirect enum AppItem: Hashable, Identifiable {
var id: Int {
hashValue
}
case foobar, profile, welcome, articles, books, websites
case nested(item: AppItem, nested: [AppItem])
@ViewBuilder
var destination: some View {
switch self {
case .foobar:
FoobarOverview()
case .profile:
Profile()
case .welcome:
Welcome()
case .articles:
Overview(navTitle: "Articles")
case .books:
Overview(navTitle: "Books")
case .websites:
Overview(navTitle: "Websites")
case .nested:
EmptyView()
}
}
var name: String {
switch self {
case .foobar:
return "Foobar"
case .profile:
return "Profile"
case .welcome:
return "Welcome"
case .articles:
return "Articles"
case .books:
return "Books"
case .websites:
return "Websites"
case .nested(let item, _):
return item.name
}
}
static var ipad: [AppItem] = [.welcome, .profile, .nested(item: .foobar, nested: overviews)]
static var iphone: [AppItem] = [.welcome, .profile, .foobar]
static var overviews: [AppItem] = [.articles, .books, .websites]
}
iPhone Layout
struct iPhoneEntry: View {
@State var selectedItem: AppItem? = .welcome
@ViewBuilder
var body: some View {
TabView(selection: $selectedItem) {
ForEach(AppItem.iphone) { item in
switch item {
case .foobar:
NavigationView {
FoobarOverview()
}
.tag("Overview")
.tabItem { Label("Overview", systemImage: "circle") }
case .profile:
Profile()
.tag("Profile")
.tabItem { Label("Profile", systemImage: "triangle") }
case .welcome:
Welcome()
.tag("Welcome")
.tabItem { Label("Welcome", systemImage: "square") }
default:
EmptyView()
}
}
}
}
}
iPad-Layout
struct iPadEntry: View {
@State var selectedItem: AppItem? = .welcome
var body: some View {
NavigationView {
List {
ForEach(AppItem.ipad) { item in
if case .nested(let item, let subItems) = item {
Section {
ForEach(subItems, content: ListItem)
} header: {
Text(item.name)
}
} else {
ListItem(item)
}
}
}
.listStyle(.sidebar)
.modifier(ColumnModifier(appItem: selectedItem))
}
.navigationViewStyle(.columns)
}
func ListItem(_ appItem: AppItem) -> some View {
NavigationLink(tag: appItem, selection: $selectedItem) {
appItem.destination
} label: {
Text(appItem.name)
}
}
private struct ColumnModifier: ViewModifier {
let appItem: AppItem?
func body(content: Content) -> some View {
if appItem == .welcome || appItem == .profile {
Group {
content
Text("Select something")
}
} else {
Group {
content
appItem?.destination
Text("Select something")
}
}
}
}
}
Dummy Views
struct Profile: View {
var body: some View {
Text("Profile")
}
}
struct Welcome: View {
var body: some View {
Text("Welcome")
}
}
struct FoobarOverview: View {
var body: some View {
List {
ForEach(AppItem.overviews) { item in
NavigationLink(item.name, destination: item.destination)
}
}
}
}
struct Overview: View {
var navTitle: String
var body: some View {
List {
ForEach(0 ... 10, id: \.self) { num in
NavigationLink("\(navTitle) number: \(num)") {
Detail(title: "\(navTitle) number: \(num)")
}
}
}
}
}
struct Detail: View {
var title: String
var body: some View {
Text("foobar")
}
}
EDIT: I want to know how I can navigate 6 levels deep for iPad and iPhone without implementing two layouts.
NavigationSplitView
cannot have a dynamic number of columns, and I don't think NavigationView
officially supported this anyway.
That said, you can achieve a similar kind of behaviour by wrapping two NavigationSplitView
s with an if
. i.e. show the three-column split view if the selected item is one of the ones in AppItem.overviews
, otherwise show the two-column split view.
struct iPadEntry: View {
@State var selectedItem: AppItem? = .welcome
var body: some View {
if let selectedItem, AppItem.overviews.contains(selectedItem) {
NavigationSplitView {
SplitSidebar(selectedItem: $selectedItem)
} content: {
SplitDetail(selectedItem: selectedItem)
} detail: {
NavigationStack {
Text("Select something")
}
}
} else {
NavigationSplitView {
SplitSidebar(selectedItem: $selectedItem)
} detail: {
NavigationStack {
SplitDetail(selectedItem: selectedItem)
}
}
}
}
}
struct SplitSidebar: View {
@Binding var selectedItem: AppItem?
var body: some View {
List(selection: $selectedItem) {
ForEach(AppItem.ipad) { item in
if case .nested(let item, let subItems) = item {
Section {
ForEach(subItems) { subItem in
NavigationLink(value: subItem) {
Text(subItem.name)
}
}
} header: {
Text(item.name)
}
} else {
NavigationLink(value: item) {
Text(item.name)
}
}
}
}
.listStyle(.sidebar)
}
}
struct SplitDetail: View {
let selectedItem: AppItem?
var body: some View {
if let selectedItem {
selectedItem.destination
} else {
Text("Select something")
}
}
}
That said, I would recommend redesigning this to not use a dynamic number of columns. Consider navigating to the overview view in the same column, or nest another two-column split view in the detail column.
Here are the other views:
struct iPhoneEntry: View {
@State var selectedItem: AppItem = .welcome
@ViewBuilder
var body: some View {
TabView(selection: $selectedItem) {
ForEach(AppItem.iphone) { item in
switch item {
case .foobar:
NavigationStack {
FoobarOverview()
}
.tabItem { Label("Overview", systemImage: "circle") }
case .profile:
Profile()
.tabItem { Label("Profile", systemImage: "triangle") }
case .welcome:
Welcome()
.tabItem { Label("Welcome", systemImage: "square") }
default:
EmptyView()
}
}
}
}
}
struct FoobarOverview: View {
var body: some View {
List {
ForEach(AppItem.overviews) { item in
NavigationLink(item.name) { item.destination }
}
}
}
}
struct Overview: View {
var navTitle: String
var body: some View {
List {
ForEach(0 ... 10, id: \.self) { num in
NavigationLink("\(navTitle) number: \(num)") {
Detail(title: "\(navTitle) number: \(num)")
}
}
}
}
}
Side note: you should declare AppItem.id
as:
var id: AppItem { self }
Do not use the hash value as an identifier (for anything), because hash collisions exist.