How to Build macOS System-Like App Settings in SwiftUI

4 min read Original article ↗

Schopenlaam

Press enter or click to view image in full size

App Settings for Mouse Pro

Recently, when I was using SwiftUI to build a macOS app called Mouse Pro, I have created a setting window similar to System Settings, just like the screenshot above.

However, I found it not so easy. Since Apple does not directly provide a SwiftUI component for that, so I can only achieve it by combining various components, I’ll show you how to do with it.

Step 1. NavigationSplitView

The core for this layout is NavigationSplitView, the sidebar contains a menu, and the settings view on the right side.

struct ContentView: View {
var body: some View {
NavigationSplitView(columnVisibility: .constant(.doubleColumn)) {
Text("Menu")
.frame(width: 215)
.toolbar(removing: .sidebarToggle)
} detail: {
Text("Settings")
}
.frame(minWidth: 715, maxWidth: 715, minHeight: 470, maxHeight: .infinity)
}
}
@main
struct macOS_SampleApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.windowResizability(.contentSize)
.windowStyle(.hiddenTitleBar)
}
}

Now here’s what you got

Press enter or click to view image in full size

NavigationSplitView

There are some magic numbers and arguments in the code samples:

  1. columnVisibility: .constant(.doubleColumn): Make sure both two columns of the view are always visible, espicially we will hide the sidebar toggle button in the following step.
  2. toolbar(removing: .sidebarToggle): Hide the sidebar toggle button
  3. 215, 715 and 470: Yes, I’ve measured it for you, the width of sidebar menu in System Settings if 215, and the size of whole window is 710x470, and you can adjust the height ≥ 470.

Step 2. List and NavigationLink

Before we create the sidebar menu, let’s create two views for upcoming usage.

struct FooView: View {
var body: some View {
Text("Foo View")
}
}

struct BarView: View {
var body: some View {
Text("Bar View")
}
}

Now it’s the sidebar menu built with List and NavigationLink, link to the two views we’ve just created.

struct ContentView: View {
var body: some View {
NavigationSplitView(columnVisibility: .constant(.doubleColumn)) {
List {
NavigationLink(destination: FooView()) {
Label("Foo", systemImage: "shippingbox")
}
NavigationLink(destination: BarView()) {
Label("Bar", systemImage: "gearshape")
}
}
.padding(.top)
.frame(width: 215)
.toolbar(removing: .sidebarToggle)
} detail: {
FooView()
}
.frame(minWidth: 715, maxWidth: 715, minHeight: 470, maxHeight: .infinity)
}
}

Default detail view is required in macOS 15.0+

For simplicity, the NavigationLink is hard coded, in pratical, you should use List(_ data: selection: rowContent: ) instead, so does the default detail view here.

Now your window should look like this

Press enter or click to view image in full size

Sidebar Menu with List and NavigationLink

Step 3. GroupBox

So far, what we need is almost done. Since the following content depends on actual needs. I’ll only introduce the most commonly used component — GroupBox, because it’s widely used in System Settings.

Now let’s modify our FooView

struct FooView: View {
@State private var restartNeeded = true
@State private var enable = true
@State private var display = 0
@State private var launchAtLogin = false

var body: some View {
ScrollView {
if restartNeeded {
GroupBox {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
Text("Restart to take effect")
Spacer()
Button("Restart") {
Swift.print("Restart")
}
}
.padding(4)
.foregroundColor(.yellow)
}
.padding(.horizontal)
}
GroupBox {
VStack {
HStack {
Text("Enable")
Spacer()
Toggle("", isOn: $enable)
.toggleStyle(.switch)
}
Divider()
HStack {
Text("Display in")
Spacer()
Picker(selection: $display, label: Text("")) {
Text("Both MenuBar and Dock").tag(0)
Text("MenuBar").tag(1)
Text("Dock").tag(2)
}
.scaledToFit()
}
Divider()
HStack {
Text("Launch at Login")
Spacer()
Toggle("", isOn: $launchAtLogin)
.toggleStyle(.switch)
}
}
.padding(4)
}
.padding(.horizontal)
GroupBox {
HStack {
Text("Reset All Settings")
Spacer()
Button("Reset and Restart") {
Swift.print("Reset and Restart")
}
}
.padding(4)
}
.padding(.horizontal)
.padding(.bottom)
}
.padding(.top)
}
}

By the end of this step, you’ll get this view

Press enter or click to view image in full size

Custom Setting View built with GroupBox

Most of the magic things here is based on the design of System Settings. It is worth noting that the labels of the controls themself are all set to empty strings, and don’t forget to use sacledToFit() for the Picker.

Finally, all components should be wrap inside a ScrollView, so that it can scroll up and down when there are too many setting options.