Instantly share code, notes, and snippets.
Forked from Dimillian/NavigationSplitViewExample.swift
Last active
November 7, 2024 01:49
-
Star
4
(4)
You must be signed in to star a gist -
Fork
0
(0)
You must be signed in to fork a gist
-
Save nkalvi/4cdc746ab92b5da664621d61ac7690e9 to your computer and use it in GitHub Desktop.
An example on how to use the new NavigationSplitView on iPad with global navigation - updated to work by following "Bringing robust navigation structure to your SwiftUI app"
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 2024-11-06: Simple test based on "Bringing robust navigation structure to your SwiftUI app" | |
// https://developer.apple.com/documentation/swiftui/bringing_robust_navigation_structure_to_your_swiftui_app | |
// Xcode Version 16.1, iOS 17+ | |
// Opens to a sheet first instead of NavigationSplitView | |
import SwiftUI | |
@main | |
struct TestSplitViewApp: App { | |
private var navigationModel = NavigationModel(sidebarDestination: .subreddit(subreddit: .games)) | |
var body: some Scene { | |
WindowGroup { | |
ContentView() | |
} | |
.environment(navigationModel) | |
} | |
} | |
@Observable | |
final class NavigationModel { | |
var sidebarDestination: Destination? | |
var detailPath: [Destination] | |
var detailNavigation: Destination? | |
var columnVisibility: NavigationSplitViewVisibility | |
var showSplitView: Bool = false | |
var showLoginView: Bool = false | |
init(sidebarDestination: Destination? = nil, detailPath: [Destination] = [], detailNavigation: Destination? = nil, columnVisibility: NavigationSplitViewVisibility = .doubleColumn) { | |
self.sidebarDestination = sidebarDestination | |
self.detailPath = detailPath | |
self.detailNavigation = detailNavigation | |
self.columnVisibility = columnVisibility | |
} | |
var selectedDetail: Destination? { | |
get { detailPath.first } | |
set { detailPath = [newValue].compactMap { $0 } } | |
} | |
} | |
struct LoginView: View { | |
@Environment(\.dismiss) private var dismiss | |
@Environment(NavigationModel.self) var navigationModel | |
var body: some View { | |
VStack { | |
Text("Login") | |
.padding() | |
.navigationBarTitle("Login") | |
Button("Continue") { | |
navigationModel.showSplitView = true | |
dismiss() | |
} | |
} | |
} | |
} | |
struct ContentView: View { | |
@Environment(NavigationModel.self) var navigationModel | |
var body: some View { | |
@Bindable var navigationModel = navigationModel | |
Group { | |
if navigationModel.showSplitView { | |
MainView() | |
} else { | |
EmptyView() | |
} | |
} | |
.onAppear { | |
navigationModel.showLoginView = true | |
} | |
.sheet(isPresented: $navigationModel.showLoginView) { | |
LoginView() | |
} | |
} | |
} | |
struct MainView: View { | |
@Environment(NavigationModel.self) private var navigationModel | |
var body: some View { | |
@Bindable var navigationModel = navigationModel | |
NavigationSplitView( | |
columnVisibility: $navigationModel.columnVisibility | |
) { | |
SidebarView() | |
} content: { | |
Group { | |
switch navigationModel.sidebarDestination { | |
case .home(let destination): | |
HomeView(destination: destination) | |
.navigationTitle(navigationModel.sidebarDestination!.caseName.capitalized) | |
case .subreddit(let subreddit): | |
SubredditView(subreddit: subreddit) | |
case .user(let destination): | |
AccountView(destination: destination) | |
.navigationTitle(navigationModel.sidebarDestination!.caseName.capitalized) | |
case .post(let post): | |
PostView(post: post) | |
case .none: | |
EmptyView() | |
} | |
} | |
} detail: { | |
NavigationStack { | |
Group { | |
if case .subreddit = navigationModel.sidebarDestination { | |
if let detailNavigation = navigationModel.selectedDetail { | |
if case .post(let post) = detailNavigation { | |
PostView(post: post) | |
} | |
} else { | |
Text("Please select a post") | |
} | |
} else { | |
EmptyView() | |
} | |
}.navigationDestination(for: Destination.self) { destination in | |
switch destination { | |
case .user(let userDestination): | |
AccountView(destination: userDestination) | |
default: | |
Text("Not supported here") | |
} | |
} | |
.toolbar { | |
Button("Login") { | |
navigationModel.showSplitView = false | |
navigationModel.showLoginView = true | |
} | |
} | |
} | |
} | |
} | |
} | |
struct SidebarView: View { | |
@Environment(NavigationModel.self) var navigationModel | |
var body: some View { | |
@Bindable var navigationModel = navigationModel | |
List(selection: $navigationModel.sidebarDestination) { | |
Section("Home") { | |
ForEach(HomeDestination.allCases, id: \.self) { homeItem in | |
NavigationLink(value: Destination.home(home: homeItem)) { | |
Label(homeItem.rawValue.capitalized, systemImage: "globe") | |
} | |
} | |
} | |
Section("Subreddit") { | |
ForEach(SubredditDestination.allCases, id: \.self) { subreddit in | |
NavigationLink(value: Destination.subreddit(subreddit: subreddit)) { | |
Label(subreddit.rawValue.capitalized, systemImage: "globe") | |
} | |
} | |
} | |
Section("Account") { | |
ForEach(UserDestination.allCases, id: \.self) { userDestination in | |
NavigationLink(value: Destination.user(user: userDestination)) { | |
Label(userDestination.rawValue.capitalized, systemImage: "globe") | |
} | |
} | |
} | |
} | |
.navigationTitle("Categories") | |
} | |
} | |
struct PostView: View { | |
let post: Post | |
var body: some View { | |
VStack { | |
Text(post.title) | |
.font(.title) | |
Text(post.preview) | |
NavigationLink(value: Destination.user(user: .comments)) { | |
Text("See some sub navigation") | |
} | |
} | |
} | |
} | |
struct AccountView: View { | |
let destination: UserDestination | |
var body: some View { | |
Text(destination.rawValue.capitalized) | |
} | |
} | |
struct HomeView: View { | |
let destination: HomeDestination | |
var body: some View { | |
Text(destination.rawValue.capitalized) | |
} | |
} | |
enum HomeDestination: String, CaseIterable, Hashable { | |
case hot, best, trending, new, top, rising | |
} | |
enum SubredditDestination: String, CaseIterable, Hashable { | |
case news, diablo, pics, games, movies | |
} | |
enum UserDestination: String, CaseIterable, Hashable { | |
case profile, inbox, posts, comments, saved | |
} | |
enum Destination: Hashable { | |
case home(home: HomeDestination) | |
case subreddit(subreddit: SubredditDestination) | |
case user(user: UserDestination) | |
case post(post: Post) | |
var caseName: String { | |
switch self { | |
case .home: | |
"Home" | |
case .subreddit: | |
"Subreddit" | |
case .user: | |
"Account" | |
case .post: | |
"Post" | |
} | |
} | |
} | |
struct Post: Identifiable, Hashable { | |
let id = UUID() | |
let title = "A post title" | |
let preview = "Some wall of text to represent the preview of a post that nobody will read if the title is not a clickbait" | |
} | |
extension Post { | |
static var posts: [Post] = [Post(), Post(), Post(), Post(), Post(), Post(), Post(), Post()] | |
} | |
struct SubredditView: View { | |
let subreddit: SubredditDestination | |
@Environment(NavigationModel.self) var navigationModel | |
var body: some View { | |
@Bindable var navigationModel = navigationModel | |
List(Post.posts, selection: $navigationModel.selectedDetail) { post in | |
NavigationLink(value: Destination.post(post: post)) { | |
HStack { | |
VStack(alignment: .leading) { | |
Text(post.title) | |
.font(.title3) | |
.fontWeight(.semibold) | |
Text(post.preview) | |
.font(.callout) | |
} | |
} | |
} | |
}.navigationTitle(subreddit.rawValue.capitalized) | |
} | |
} | |
#Preview("LoginView") { | |
@Previewable var navigationModel = NavigationModel(sidebarDestination: .subreddit(subreddit: .games)) | |
LoginView() | |
.environment(navigationModel) | |
} | |
#Preview("SidebarView") { | |
@Previewable var navigationModel = NavigationModel(sidebarDestination: .subreddit(subreddit: .games)) | |
SidebarView() | |
.environment(navigationModel) | |
} | |
#Preview("PostView") { | |
@Previewable var navigationModel = NavigationModel(sidebarDestination: .subreddit(subreddit: .games)) | |
PostView(post: .posts.first!) | |
.environment(navigationModel) | |
} |
Thank you so much.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi @SF-Simon , I've updated the sample using
Observable
and added a simple opening sheet instead ofNavigationStack
. Tested on iPad simulator and a real device. Please see whether it crashes.BTW, I was happy to see Apple sample code has been recently updated; please check it out - it is a good one.
Bringing robust navigation structure to your SwiftUI app | Apple Developer Documentation