When I first evolved Clora For macOS I use the same navigation model as on iOS and iPadOS, Navigation links It has flaws in macOS: when users hide the sidebar we can not change the navigation programmatically. Each time the user changed navigation, SwiftUI saw in the details view a new display structure even when there was only a data change.
Watching b WWDC21 SwiftUI on Mac The session I noticed instead of using Navigation links They passed a
selection Binding the
List Combined with
.tag On the items that can be selected in the list. Then in detail use b
selection Control what is displayed.
A simple list
First we need to store the current
selection somewhere. I find it better to use
SceneStorage Property wrapper so we get automatic mode recovery.
@SceneStorage("com.example.myapp.selection") var selection: UUID?
The type of data you use to identify your selection will depend on your data model, see How to Save Custom Code Types in SceneStorage.
In simple situations we can do the following.
struct SideBar: View @Binding var selection: UUID? var body: some View List(selection: $selection) ForEach(items) item _ ItemSidebarView(item: item).tag(item.id) .listStyle(.sidebar)
Then the main details area of the app takes
selection As well.
struct MainDetailView: View let selection: UUID? var body: some View if let selection = self.selection ItemDetailView(id: selection) else NothingSelectedView()
With the root
ContentView Holds the
struct ContentView: View @SceneStorage("com.example.myapp.selection") var selection: UUID? var body: some View NavigationView SideBar(selection: $selection) MainDetailView(selection: selection)
Handling nested items
In macOS it is common to have expandable groups inside the sidebar, which contain a tree structure of your application data. For this SwiftUI provides several different ways to create the tree. There is a constructor for
List Which allows us to move not only an array of items but also a
keyPath To retrieve descendant items unfortunately, it does not support programmatic control in the expansion of these groups. So I prefer to use
DisclosureGroup We can move a cover that indicates if the item is extended. MacOS users are accustomed to the activity of multiple extensions. A
Set Of extensible IDs is perfect for this storage.
@SceneStroage("com.example.myapp.expansion") var expansion: Set<UUID> =  List(selection: $selection) ForEach(items) item _ DisclosureGroup( isExpanded: $expansion.for(value: item.id) ) ... label: ItemSidebarView(item: item) .tag(item.id)
We also need to announce an extension
Binding Which allows us to create a
Binding<Bool> Our own
extension Binding where Value == Set<UUID> func for(value: UUID) -> Binding<Bool> Binding<Bool> self.wrappedValue.contains(value) set: newValue in if newValue self.wrappedValue.insert(value) else self.wrappedValue.remove(value)
Using more than one object type that can be selected in the sidebar is common in macOS applications. We need to make a change in the type of data stored in it
SceneStorage To contain it.
Let’s take a look at a writing app with some static navigation goals and a list of articles.
enum Selection case all case lastSevenDays case trash case inbox case article(id: UUID)
In this case, ours
selection The value is stored as:
@SceneStroage("com.example.myapp.selection") var selection: Selection?
In the main DetailView we can use the switch statement to select what view is:
switch selection case .all: AllArticlesView() case .lastSevenDays: LastSevenDaysArticlesView() case .trash: TrashView() case .inbox: InboxView() case .article(let id): ArticleView(id: id)