MacOS Development Handbook

A Common Mac Application with a Sidebar and Content Area

In our main content view, we can use NavigationView as our base body. A navigation view in MacOS will display two separated views, a sidebar at left and content view at right.

1
2
3
4
5
6
7
8
9
10
11
12
NavigationView {
SidebarView()
DetailContentView()
}
.frame(
minWidth: 700,
idealWidth: 1000,
maxWidth: .infinity,
minHeight: 400,
idealHeight: 800,
maxHeight: .infinity
)

Of course, the window’s frame definition is necessary for appropriate window layout.

Creating a List as the Sidebar

The List component in SwiftUI is used to create a list view with multiple item cells.

1
2
3
4
5
6
7
8
List(selection: $selection) {
Section("Section One") {
ForEach(cellData) { element in
Text(element.title)
}
}
}
.listStyle(.sidebar)

We can use ForEach to iterate through all our cell data and display them in the list view.
We use Section to separate our cells into several different groups, these groups can be collapsed respectively.

Setting the list style by using the modifier of listStyle. There are many build-in list styles you can use. More details you can check the official document List Styles

In desktop devices, users normally use mouses or tack pads for controlling the cursor. When the cursor moves over a clickable element in our applications, we should give some tips for indicating you can click it. In SwiftUI, the onHover modifier is designed for this.

1
2
3
4
5
6
7
.onHover { inside in
if inside {
NSCursor.pointingHand.push()
} else {
NSCursor.pop()
}
}

The block will be invoked whenever the cursor move in or out the decorated view’s area.

Sand Box

In iOS, every application runs in an isolated sand box. The application doesn’t have access of data of other applications. But in MacOS, applications can share data with the file system. For accessing local or remote data, we have to enable the App Sandbox capability by check the options in Target -> Signing & Capabilities -> App Sandbox.

Checking the Outgoing Connections (Client) box, if you just want to make a network request with URLSession.

Adding Menus

If you want to add some menus for your application, you need to add a commands modifier to the WindowGroup view. The modifier needs you to return some Commands views. The Commands is a protocol and we can construct a new View struct complying it to customize ourselves menus.

There are many build-in commands we can use directly. (Commands)[https://developer.apple.com/documentation/swiftui/commands]

  • EmptyCommands
  • ImportFromDevicesCommands
  • SidebarCommands
  • TextEditingCommands
  • TextFormattingCommands
  • ToolbarCommands

There are two exceptive items, CommandGroup and CommandMenu. They are both used to make new command menus, but the CommandMenu is for creating a stand-alone top level menu, the CommandGroup is for adding additional commands to existing commands menus.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
struct Menus: Commands {

var body: some Commands {
SidebarCommands()
ToolbarCommands()

CommandGroup(replacing: .help) {
Button("Official Web") {
NSWorkspace.shared.open(URL(string: "http://www.csl.cool")!)
}
.keyboardShortcut("/", modifiers: .command)
}

CommandMenu("Display") {
Toggle(isOn: $showTotals) {
Text("Show Totals")
}
.keyboardShortcut("t", modifiers: .command)
}

Picker("Appearance", selection: $displayMode) {
ForEach(DisplayMode.allCases, id: \.self) {
Text($0.rawValue)
.tag($0)
}
}

Divider()

Menu("Appearance") {
Button("Light") {
displayMode = .light
}
.keyboardShortcut("L", modifiers: .command)
}
}
}

In command menus, we usually use Toggle, Picker and Button to crate our custom controls. The Toggle is for boolean values, the Picker is for enum values, the Button is for normal commands.

We can use the .keyboardShortcut modifier to add shortcut to our commands.

AppStorage and SceneStorage

Swift provides two tools for easily writing, reading or observing UserDefaults.
The AppStorage saves preferences of the whole application. The SceneStorage saves preferences separately for multiple windows for the application.

Adding Toolbars

We usually add a toolbar onto the content view, so we add a toolbar modifier to the DetailContentView. Like the commands we used above, the toolbar modifier needs us to return ToolbarContent views.

1
2
3
4
5
6
7
8
9
10
11
12
struct Toolbar: ToolbarContent {
var body: some ToolbarContent {
ToolbarItem(placement: .navigation) {
Button {

} label: {
Image(systemName: "sidebar.left")
}
.help("Toggle Sidebar")
}
}
}