List Selection Based Navigation on macOS

Related Articles

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.

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 
  var selection: UUID?
  var body: some View 
    List(selection: $selection) 
      ForEach(items)  item _
        ItemSidebarView(item: item).tag(

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)

With the root ContentView Holds the SceneStorage selection:

struct ContentView: View 
  var selection: UUID?
  var body: some View 
      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 DisclosureGroups.

When using 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.

var expansion: Set<UUID> = []

List(selection: $selection) 
 ForEach(items)  item _
      isExpanded: $expansion.for(value:
       ItemSidebarView(item: item)

We also need to announce an extension Binding Which allows us to create a Binding<Bool> Our own Binding<Set<UUID>> Type.

extension Binding where Value == Set<UUID> 
  func for(value: UUID) -> Binding<Bool> 
     set:  newValue in
      if newValue 

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:

var selection: Selection?

In the main DetailView we can use the switch statement to select what view is:

switch selection 
  case .all:
  case .lastSevenDays:
  case .trash:
  case .inbox:
  case .article(let id):
    ArticleView(id: id)



Please enter your comment!
Please enter your name here

Popular Articles