Multi Section Sidebar using List with selections for macOS

I'm trying to implement a 3 column NavigationSplitView in SwiftUI on macOS - very similar to Apple's own NavigationCookbook sample app - with the slight addition of multiple sections in the sidebar similar to how the Apple Music App has multiple sections in the sidebar.

Note: This was easily possible using the deprecated

NavigationLink(tag, selection, destination) API

The most obvious approach is to simply do something like:

		NavigationSplitView(sidebar: {
			List {
				Section("Section1") {
					List(section1, selection: $selectedItem1) {
						item in
						NavigationLink(item.label, value: item)
					}
				}
				Section("Section2") {
					List(section2, selection: $selectedItem2) {
						item in
						NavigationLink(item.label, value: item)
					}
				}
			}
		},
		content: {
			Text("Content View")
		}, detail: {
			Text("Detail View")
		})

But unfortunately, this doesn't work - it doesn't seem to properly iterate over all of the items in each List(data, selection: $selected) or the view is strangely cropped - it only shows 1 item. However if the 1 item is selected, then the appropriate bindable selection value is updated. See image below:

If you instead use ForEach for enumerating the data, that does seem to work, however when you use ForEach, you loose the ability to track the selection offered by the List API, as there is no longer a bindable selection propery in the NavigationLink API.

		NavigationSplitView(sidebar: {
			List {
				Section("Section1") {
					ForEach(section1) {
						item in
						NavigationLink(item.label, value: item)
					}
				}
				Section("Section2") {
					ForEach(section2) {
						item in
						NavigationLink(item.label, value: item)
					}
				}
			}
		},
		content: {
			Text("Content View")
		}, detail: {
			Text("Detail View")
		})

We no longer know when a sidebar selection has occurred.

See image below:

Obviously Apple is not going to comment on the expected lifespan of the now deprecated API - but I am having a hard time switching to the new NavigationLink with a broken sidebar implementation.

@EulerDev You would want to stucture your underlying data with a level of hierarchy which would allow you structure your list this way:

List {
     ForEach(company.departments) { department in
         Section(header: Text(department.name)) {
             ForEach(department.staff) { person in
                PersonRowView(person: person)
             }
         }
     }
 }

Please review: Represent data hierarchy with sections and let me know if you any further questions.

I don't think I did a good job at explaining the actual issue. I think I have come up with a good solution that works for the new @Observable macro and also does not rely on the deprecated NavigationLink(tag, selection) API.

The issue is this:

It is common to show multiple lists in multipanel sidebar apps (i.e., Apple Music, Apple Mail) - and one way of keeping track of selection changes of items in the sidebar was to use the now deprecated NavigationLink(tag, selection) API - as this could be used to bind to a variable in your appModel that keeps track of user selections in the Sidebar.

Now that that version of the NavigationLink API has been deprecated, it was not clear how to keep track of the current selected item in multi-section sidebar lists. (Like Apple Music, Apple Mail, etc). Your example is not helpful because it doesn't show how you would create a binding to some data to keep track of the currently selected item in the sidebar.

The NavigationCookbook SwiftUI sample app is not helpful either because it doesn't show use of a multi-section sidebar. It uses the List(selection, content) API and uses a binding variable for selection to keep track of the selected item in the sidebar - but doesn't show how this could be accomplished with multiple unrelated lists in the Sidebar.

The problem with multi section sidebar items (especially when there are multiple sections of completely unrelated data (i.e.,Apple Music App sidebar) - is you can no longer use List(selection, content) as it seems Lists within Lists are not supported?

I am not sure if I am missing something simple, but it seems like the only way to keep track of the currently selected sidebar item with modern @Observable data and Swift Concurrency framework is to use the Destination view to update the state variable for keeping track of what is selected in the Sidebar. You have to use both .onAppear{} and .onChange(of:) to track these changes. I am not sure if this is by design - but there really is no other way to keep track of the selected Sidebar item. The downside of this solution is that it requires there to be a Destination view for Sidebar items - you can't seeminly have a Sidebar item that just changes state and does not launch a view.

I will post a full working skeleton below that I assume is the preferred way moving forward with the now deprecated NavigationLink APIs and the new @Observable macro with their modified getters and setters.

import SwiftUI

// MARK: - Data Section

enum Mode: String, CaseIterable, Identifiable {
	case home = "Home"
	case browse = "Browse"
	case radio = "Radio"
	var id: String { rawValue }

	var icon: String {
		switch self {
		case .home:
			return "house"
		case .browse:
			return "square.grid.2x2"
		case .radio:
			return "dot.radiowaves.left.and.right"
		}
	}
}

enum Library: String, CaseIterable, Identifiable {
	case recentlyAdded = "Recently Added"
	case artists = "Artists"
	case albums = "Albums"
	case songs = "Songs"
	var id: String { rawValue }
	var icon: String {
		switch self {
		case .recentlyAdded:
			return "clock"
		case .artists:
			return "music.mic"
		case .albums:
			return "square.stack"
		case .songs:
			return "music.note"
		}
	}
}

@Observable
class MusicAppModel {
	var selectedLibrary: String? { didSet {
		if selectedLibrary != nil {
			// we can't have multiple selections in the Sidebar
			selectedMode = nil
			print("selectedLibrary = \(selectedLibrary ?? "None")")
		}
	}}
	var selectedMode: String? { didSet {
		if selectedMode != nil {
			// we can't have multiple selections in the Sidebar
			selectedLibrary = nil
			print("selectedMode = \(selectedMode ?? "None")")
		}
		
	}}
	var displayedIcon: String?
	
	// used for testing event propagation on sidebar selection property change
	// can these be called using didSet for their respective properties with the new @Observable? Not sure
	// For now these are being called when the values are detected as changed in the corresponding destination view
	func onSelectedLibraryChange(newLibrary: Library) {
		selectedLibrary = newLibrary.rawValue
		displayedIcon = newLibrary.icon
	}
	
	// used for testing event propagation on sidebar selection property change
	func onSelectedModeChange(newMode: Mode) {
		print("mode change = \(newMode)")
		selectedMode = newMode.rawValue
		displayedIcon = newMode.icon
	}
}


// MARK: - View Section

// your main WindowGroup() should call this view
struct ContentView: View {
	@State private var appModel = MusicAppModel()
	var body: some View {
		SidebarExampleUsingUpdatedNavLink()
			.environment(appModel)
	}
}

struct ModeContentView: View {
	@Environment(MusicAppModel.self) private var appModel
	var mode: Mode
	var body: some View {
		HStack {
			if let icon = appModel.displayedIcon {
				Image(systemName: icon)
			}
			Text("Selected Mode: \(mode.rawValue)")
		}
		// onChange is not called the first time a mode is selected - must use .onAppear
		.onChange(of: mode) { oldValue, newValue in
			appModel.onSelectedModeChange(newMode: newValue)
		}
		.onAppear {
			appModel.onSelectedModeChange(newMode: mode)
		}
	}
}

struct LibraryContentView: View {
	@Environment(MusicAppModel.self) private var appModel
	var library: Library
	var body: some View {
		HStack {
			// this shows that our app model is getting property updates when selection changes
			if let icon = appModel.displayedIcon {
				Image(systemName: icon)
			}
			Text("Selected Library: \(library.rawValue)")
		}
		// onChange is not called the first time a library is selected - must use .onAppear
		.onChange(of: library) { oldValue, newValue in
			appModel.onSelectedLibraryChange(newLibrary: newValue)
		}
		.onAppear {
			appModel.onSelectedLibraryChange(newLibrary: library)
		}
	}
}

struct SidebarExampleUsingUpdatedNavLink: View {
	@Environment(MusicAppModel.self) private var appModel
	var body: some View {
		@Bindable var appModel = appModel
		NavigationSplitView(sidebar: {
			List {
				Section("Mode", content: {
					ForEach(Mode.allCases) {
						mode in
						NavigationLink(destination: {
							ModeContentView(mode: mode)
								.navigationTitle(mode.rawValue)
						}, label: {
							HStack {
								Image(systemName: mode.icon)
								Text(mode.rawValue)
								Spacer()
							}
						})
					}
				})
				
				Section("Libarary", content: {
					ForEach(Library.allCases) {
						library in
						NavigationLink(destination: {
							LibraryContentView(library: library)
								.navigationTitle(library.rawValue)
						}, label: {
							HStack {
								Image(systemName: library.icon)
								Text(library.rawValue)
								Spacer()
							}
						})
					}
				})
			}
			
		}, detail: {
			VStack {
				Text("Please select an item in the sidebar!")
					.navigationTitle("Sidebar Selection Binding")
			}
		})
	}
}

#Preview {
	struct AppleMusicExamplePreview: View {
		var body: some View {
			ContentView()
				.preferredColorScheme(.dark)
				.frame(width: 800, height: 800)
		}
	}
	return AppleMusicExamplePreview()
}
Multi Section Sidebar using List with selections for macOS
 
 
Q