Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save nkalvi/4cdc746ab92b5da664621d61ac7690e9 to your computer and use it in GitHub Desktop.

Select an option

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

SF-Simon commented Nov 6, 2024

Copy link
Copy Markdown

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

nkalvi commented Nov 6, 2024

Copy link
Copy Markdown
Author

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

SF-Simon commented Nov 6, 2024

Copy link
Copy Markdown

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

nkalvi commented Nov 6, 2024

Copy link
Copy Markdown
Author

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

SF-Simon commented Nov 7, 2024

Copy link
Copy Markdown

Thank you so much.

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