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)
}
@SF-Simon
Copy link

SF-Simon commented Nov 6, 2024

Hello, @nkalvi , thank you for providing such a good example. I have encountered a difficult problem now, and I am not sure if it is a bug.
Our application only has three columns after logging in, so I made some changes to the code.

struct ContentView: View {
    @EnvironmentObject var navigationModel: NavigationModel
    
    var body: some View {
        NavigationStack(){ // Here
            NavigationLink(value: true) {
                Text("Open SplitView")
            }
            .navigationDestination(for: Bool.self) { bool in
                MainView()
            }
        }
    }
}

I used NavigationStack when logging in, and the adverse reaction I experienced was that the click on Text ("See some sub navigation") became unresponsive.

Can you tell me where the code went wrong? thank you

@nkalvi
Copy link
Author

nkalvi commented Nov 6, 2024

Hi @SF-Simon, I tested your sample with Xcode 16.1 and simulators running 18.1.
Though it didn't become unresponsive, it behaved strangely. May I ask what are your targets (iPhone/iPad/Mac)?

This gist definitely needs updating - one thing I noticed is when in iPhone portrait mode, the following should be omitted in MainView:

            .onDisappear {
                if navigationModel.selectedDetail == nil {
                    navigationModel.sidebarDestination = nil
                }
            }

I'll work on this when I get some time this week and update you.

@SF-Simon
Copy link

SF-Simon commented Nov 6, 2024

Hi @SF-Simon, I tested your sample with Xcode 16.1 and simulators running 18.1. Though it didn't become unresponsive, it behaved strangely. May I ask what are your targets (iPhone/iPad/Mac)?

This gist definitely needs updating - one thing I noticed is when in iPhone portrait mode, the following should be omitted in MainView:

            .onDisappear {
                if navigationModel.selectedDetail == nil {
                    navigationModel.sidebarDestination = nil
                }
            }

I'll work on this when I get some time this week and update you.

Our target platform is iPad 18+, and only iPads have the split view. The SplitView will appear after logging in, and the login page is a NavigationStack.

Thank you for your help.

@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