Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save nkalvi/4cdc746ab92b5da664621d61ac7690e9 to your computer and use it in GitHub Desktop.
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"
// 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)
}
@nkalvi
Copy link
Author

nkalvi commented Nov 6, 2024

Hi @SF-Simon , I've updated the sample using Observable and added a simple opening sheet instead of NavigationStack. 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

@SF-Simon
Copy link

SF-Simon commented Nov 7, 2024

Thank you so much.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment