
Macos Settings Ui
- 291 installs
- 641 repo stars
- Updated May 27, 2026
- fayazara/macos-app-skills
Helps with ai & agent building tasks.
About
macos-settings-ui is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- macos-settings-ui
- AI & Agent Building
- AI-coding skill
Macos Settings Ui by the numbers
- 291 all-time installs (skills.sh)
- Ranked #2,300 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/fayazara/macos-app-skills --skill macos-settings-uiAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 291 |
|---|---|
| repo stars | ★ 641 |
| Last updated | May 27, 2026 |
| Repository | fayazara/macos-app-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
macOS Settings UI with Liquid Glass
This skill produces a native macOS settings window that follows Apple's macOS 26 design guidelines. The result is a sidebar + detail NavigationSplitView with liquid glass window chrome, back/forward toolbar navigation, grouped Form sections with transparent backgrounds, and proper scroll edge effects.
Architecture Overview
The settings UI is composed of 3 layers:
1. Window Controller (SettingsWindowController.swift) — An NSWindowController that creates the NSWindow programmatically with .fullSizeContentView. This is what gives the window rounded liquid glass corners. You cannot get this effect from a SwiftUI Window scene.
2. Root View (SettingsView.swift) — A NavigationSplitView with a sidebar list and detail pane. Includes back/forward navigation history in the toolbar, which also forces the creation of an NSToolbar (required for the liquid glass title bar treatment).
3. Detail Panes (one file per tab) — Each pane uses Form { Section(...) { ... } }.formStyle(.grouped).scrollContentBackground(.hidden).
Why NSWindowController Instead of SwiftUI Window Scene
SwiftUI's declarative Window scene does not expose the NSWindow style mask. The .fullSizeContentView flag must be set at window creation time for macOS 26 to render the liquid glass chrome (rounded corners, translucent sidebar, blurred title bar). Trying to inject it later via NSViewRepresentable is unreliable because SwiftUI resets the window's configuration.
The NSWindowController approach also lets you control the toolbar style, frame autosave, minimum size, and delegate lifecycle directly.
Critical Modifiers
Every detail pane MUST have these three modifiers on its Form:
.formStyle(.grouped)
.scrollContentBackground(.hidden)
.contentMargins(.top, 8, for: .scrollContent).formStyle(.grouped)— gives the native inset rounded-rect section appearance.scrollContentBackground(.hidden)— makes the form background transparent so the liquid glass window chrome shows through. Without this, you get an opaque white/dark background that breaks the glass effect..contentMargins(.top, 8)— adds breathing room between the toolbar and the first section
The sidebar List MUST have:
.listStyle(.sidebar)
.scrollEdgeEffectStyleSoftIfAvailable() // macOS 26 progressive blur at scroll edges
.navigationTitle("Settings")The NavigationSplitView MUST have:
NavigationSplitView(columnVisibility: .constant(.all)) // sidebar always visible
// ...
.navigationTitle("Settings")
.navigationSplitViewStyle(.balanced)Reference Implementation
Read the reference files for complete, working code:
references/SettingsWindowController.swift— The window controller (copy as-is, adapt the activation policy calls to your app)references/SettingsView.swift— The root view with sidebar, detail routing, and navigation historyreferences/ExampleDetailPane.swift— A template detail pane showing common control patterns (Toggle, Picker, Slider, LabeledContent)
Step-by-Step: Adding Settings to a New App
1. Create the Tab Enum
Define your settings categories. Each case needs a title and SF Symbol icon:
enum SettingsTab: String, CaseIterable, Identifiable {
case general
case appearance
case about
var id: Self { self }
var title: String {
switch self {
case .general: "General"
case .appearance: "Appearance"
case .about: "About"
}
}
var systemImage: String {
switch self {
case .general: "gearshape"
case .appearance: "paintbrush"
case .about: "info.circle"
}
}
}2. Create the Window Controller
Copy references/SettingsWindowController.swift into your project. Adapt:
- The initial
contentRectsize (default700x540is good for most apps) - The
minSize(default620x460) - The activation policy calls (
AppActivationPolicy.enter()/leave()) — if your app isn't a menu-bar-only app, remove these
3. Create the Root Settings View
Copy references/SettingsView.swift. Adapt:
- The
SettingsTabenum cases to match your categories - The
SettingsDetailViewswitch to return your panes
4. Create Detail Panes
For each tab, create a pane file. Use this template:
import SwiftUI
struct GeneralSettingsPane: View {
@AppStorage("someKey") private var someValue = false
var body: some View {
Form {
Section("Section Name") {
Toggle("Toggle label", isOn: $someValue)
}
}
.formStyle(.grouped)
.scrollContentBackground(.hidden)
.contentMargins(.top, 8, for: .scrollContent)
}
}5. Open Settings from Your App
From a menu bar, button, or anywhere:
SettingsWindowController.show(tab: .general)Do NOT use a SwiftUI Window scene. Do NOT use openWindow(id:). The window controller handles everything.
6. Remove Any SwiftUI Window Scene
If you previously had a Window("Settings", id: "SETTINGS") scene in your App struct, remove it entirely. The SettingsWindowController replaces it.
Common Control Patterns Inside Form Sections
Toggle with description:
Toggle(isOn: $value) {
VStack(alignment: .leading, spacing: 2) {
Text("Primary label")
Text("Description text explaining what this does.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.toggleStyle(.switch)Picker (dropdown):
Picker("Label", selection: $value) {
Text("Option A").tag(OptionEnum.a)
Text("Option B").tag(OptionEnum.b)
}
.pickerStyle(.menu)Picker (segmented):
Picker("Label", selection: $value) {
ForEach(SomeEnum.allCases) { item in
Text(item.title).tag(item)
}
}
.pickerStyle(.segmented)Slider with value label:
LabeledContent("Size") {
HStack(spacing: 12) {
Slider(value: $size, in: 24...96, step: 2)
.frame(width: 180)
Text("\(Int(size)) pt")
.monospacedDigit()
.foregroundStyle(.secondary)
.frame(width: 46, alignment: .trailing)
}
}Button row:
HStack(spacing: 8) {
Button("Action") { doSomething() }
.controlSize(.small)
Button("Reset") { reset() }
.controlSize(.small)
.disabled(isDefault)
}Menu-Bar-Only Apps
If your app uses NSApp.setActivationPolicy(.accessory) (no Dock icon), you need activation policy management so the settings window brings the app to the foreground. The reference SettingsWindowController calls AppActivationPolicy.enter() on show and .leave() on close. Implement this as a simple reference-counting wrapper:
@MainActor
enum AppActivationPolicy {
private static var count = 0
static func enter() {
count += 1
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
}
static func leave() {
count = max(0, count - 1)
guard count == 0 else { return }
Task { @MainActor in
NSApp.setActivationPolicy(.accessory)
}
}
}If your app always shows in the Dock, remove the AppActivationPolicy calls from the window controller and just use NSApp.activate(ignoringOtherApps: true) in showWindow.
macOS Version Compatibility
The scrollEdgeEffectStyle(.soft) API is macOS 26+ only. Always wrap it in an availability check:
private extension View {
@ViewBuilder
func scrollEdgeEffectStyleSoftIfAvailable() -> some View {
if #available(macOS 26.0, *) {
scrollEdgeEffectStyle(.soft, for: .all)
} else {
self
}
}
}The rest of the pattern (NSWindowController with .fullSizeContentView, grouped Form, NavigationSplitView) works on macOS 14+.
//
// ExampleDetailPane.swift
// {{AppName}}
//
// Template showing common control patterns inside a settings detail pane.
// Every pane follows the same structure: Form > Section > controls,
// with .formStyle(.grouped) + .scrollContentBackground(.hidden).
//
import SwiftUI
// MARK: - General Settings Pane (example with toggles, buttons, labeled content)
struct GeneralSettingsPane: View {
@AppStorage("launchAtLogin") private var launchAtLogin = false
@AppStorage("exportDirectoryPath") private var exportDirectoryPath = ""
var body: some View {
Form {
Section("Save Location") {
LabeledContent("Export folder") {
HStack(spacing: 8) {
Image(systemName: "folder.fill")
.foregroundStyle(.blue)
.font(.system(size: 14))
Text(exportDirectoryPath.isEmpty ? "~/Documents" : exportDirectoryPath)
.font(.system(size: 13))
.lineLimit(1)
.truncationMode(.middle)
.foregroundStyle(.primary)
}
}
HStack(spacing: 8) {
Button("Choose Folder...") {
// Open NSOpenPanel here
}
.controlSize(.small)
Button("Use Default") {
exportDirectoryPath = ""
}
.controlSize(.small)
.disabled(exportDirectoryPath.isEmpty)
}
}
Section("System") {
// Toggle with description text
Toggle(isOn: $launchAtLogin) {
VStack(alignment: .leading, spacing: 2) {
Text("Launch at Login")
Text("Start the app automatically when you sign in.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.toggleStyle(.switch)
}
}
.formStyle(.grouped)
.scrollContentBackground(.hidden)
.contentMargins(.top, 8, for: .scrollContent)
}
}
// MARK: - Appearance Settings Pane (example with segmented picker)
struct AppearanceSettingsPane: View {
@AppStorage("appTheme") private var appTheme = "system"
var body: some View {
Form {
Section("Theme") {
Picker("Appearance", selection: $appTheme) {
Text("System").tag("system")
Text("Light").tag("light")
Text("Dark").tag("dark")
}
.pickerStyle(.segmented)
}
}
.formStyle(.grouped)
.scrollContentBackground(.hidden)
.contentMargins(.top, 8, for: .scrollContent)
}
}
// MARK: - About Settings Pane (example with app identity + links)
struct AboutSettingsPane: View {
private var versionText: String {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String
switch (version, build) {
case let (v?, b?): return "Version \(v) (\(b))"
case let (v?, nil): return "Version \(v)"
default: return "Version 1.0"
}
}
var body: some View {
Form {
Section {
HStack(alignment: .center, spacing: 16) {
Image(nsImage: NSApp.applicationIconImage)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 72, height: 72)
VStack(alignment: .leading, spacing: 6) {
Text("MyApp")
.font(.largeTitle.bold())
Text(versionText)
.font(.subheadline)
.foregroundStyle(.secondary)
Text("A brief description of your app.")
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
}
Section("Project") {
Text("A longer description of what the app does and why it exists.")
.foregroundStyle(.secondary)
Link("GitHub", destination: URL(string: "https://github.com/you/your-app")!)
}
Section("Credits") {
Text("Built by Your Name")
.foregroundStyle(.secondary)
}
}
.formStyle(.grouped)
.scrollContentBackground(.hidden)
.contentMargins(.top, 8, for: .scrollContent)
}
}
// MARK: - Advanced Example: Slider + Color Picker + Dropdown Picker
struct AdvancedSettingsPane: View {
@AppStorage("indicatorSize") private var indicatorSize = 48.0
@AppStorage("enableFeature") private var enableFeature = true
@AppStorage("outputFormat") private var outputFormat = "png"
var body: some View {
Form {
Section("Features") {
Toggle(isOn: $enableFeature) {
VStack(alignment: .leading, spacing: 2) {
Text("Enable experimental feature")
Text("This feature is still in development.")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.toggleStyle(.switch)
}
Section("Output") {
// Dropdown picker
Picker("Format", selection: $outputFormat) {
Text("PNG").tag("png")
Text("JPEG").tag("jpeg")
Text("HEIC").tag("heic")
}
.pickerStyle(.menu)
}
Section("Indicator") {
// Slider with value readout
LabeledContent("Size") {
HStack(spacing: 12) {
Slider(value: $indicatorSize, in: 24...96, step: 2)
.frame(width: 180)
Text("\(Int(indicatorSize)) pt")
.monospacedDigit()
.foregroundStyle(.secondary)
.frame(width: 46, alignment: .trailing)
}
}
}
}
.formStyle(.grouped)
.scrollContentBackground(.hidden)
.contentMargins(.top, 8, for: .scrollContent)
}
}
//
// SettingsView.swift
// {{AppName}}
//
// Root settings view with NavigationSplitView sidebar + detail,
// back/forward toolbar navigation, and liquid glass support.
//
import AppKit
import SwiftUI
// MARK: - Tab Enum
// Customize these cases, titles, and icons for your app.
enum SettingsTab: String, CaseIterable, Identifiable {
case general
case appearance
case about
var id: Self { self }
var title: String {
switch self {
case .general: "General"
case .appearance: "Appearance"
case .about: "About"
}
}
var systemImage: String {
switch self {
case .general: "gearshape"
case .appearance: "paintbrush"
case .about: "info.circle"
}
}
}
// MARK: - Navigation State (singleton so external code can set the tab)
@MainActor
@Observable
final class SettingsNavigation {
static let shared = SettingsNavigation()
var selectedTab: SettingsTab? = .general
private init() {}
}
// MARK: - Version Helper
private enum AppVersion {
static let displayString: String = {
let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0"
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "0"
return "Version \(version) (\(build))"
}()
}
// MARK: - Main Settings View
struct SettingsView: View {
@State private var navigation = SettingsNavigation.shared
@State private var navigationHistory: [SettingsTab] = [.general]
@State private var historyIndex = 0
@State private var isHistoryNavigation = false
private var activeTab: SettingsTab {
navigation.selectedTab ?? .general
}
var body: some View {
NavigationSplitView(columnVisibility: .constant(.all)) {
SettingsSidebarView(selectedTab: $navigation.selectedTab)
.frame(width: 200)
.navigationSplitViewColumnWidth(
min: 200,
ideal: 200,
max: 200
)
.toolbar(removing: .sidebarToggle)
} detail: {
SettingsDetailView(tab: activeTab)
}
.navigationTitle("Settings")
.navigationSplitViewStyle(.balanced)
.frame(minWidth: 660, minHeight: 540)
.toolbar {
ToolbarItemGroup(placement: .navigation) {
Button {
goBack()
} label: {
Image(systemName: "chevron.left")
}
.disabled(!canGoBack)
Button {
goForward()
} label: {
Image(systemName: "chevron.right")
}
.disabled(!canGoForward)
}
}
.onChange(of: navigation.selectedTab) { _, _ in
recordNavigation()
}
}
// MARK: - Navigation History
private var canGoBack: Bool {
historyIndex > 0
}
private var canGoForward: Bool {
historyIndex < navigationHistory.count - 1
}
private func goBack() {
guard canGoBack else { return }
isHistoryNavigation = true
historyIndex -= 1
navigation.selectedTab = navigationHistory[historyIndex]
DispatchQueue.main.async { isHistoryNavigation = false }
}
private func goForward() {
guard canGoForward else { return }
isHistoryNavigation = true
historyIndex += 1
navigation.selectedTab = navigationHistory[historyIndex]
DispatchQueue.main.async { isHistoryNavigation = false }
}
private func recordNavigation() {
guard !isHistoryNavigation else { return }
guard let tab = navigation.selectedTab else { return }
if navigationHistory.last == tab { return }
if historyIndex < navigationHistory.count - 1 {
navigationHistory = Array(navigationHistory.prefix(historyIndex + 1))
}
navigationHistory.append(tab)
historyIndex = navigationHistory.count - 1
}
}
// MARK: - Sidebar
private struct SettingsSidebarView: View {
@Binding var selectedTab: SettingsTab?
var body: some View {
List(selection: $selectedTab) {
ForEach(SettingsTab.allCases) { tab in
SettingsSidebarRow(tab: tab)
.tag(tab)
}
SettingsSidebarFooter()
}
.listStyle(.sidebar)
.scrollEdgeEffectStyleSoftIfAvailable()
.navigationTitle("Settings")
}
}
private struct SettingsSidebarRow: View {
let tab: SettingsTab
var body: some View {
Label {
Text(tab.title)
} icon: {
Image(systemName: tab.systemImage)
}
.foregroundStyle(.primary)
}
}
private struct SettingsSidebarFooter: View {
var body: some View {
Text(AppVersion.displayString)
.font(.footnote)
.foregroundStyle(.tertiary)
.fontDesign(.monospaced)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 6)
.padding(.vertical, 8)
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets(top: 4, leading: 0, bottom: 6, trailing: 0))
}
}
// MARK: - Detail
private struct SettingsDetailView: View {
let tab: SettingsTab
var body: some View {
Group {
switch tab {
case .general:
GeneralSettingsPane()
case .appearance:
AppearanceSettingsPane()
case .about:
AboutSettingsPane()
}
}
.navigationTitle(tab.title)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
}
// MARK: - macOS 26 Availability Helpers
private extension View {
@ViewBuilder
func scrollEdgeEffectStyleSoftIfAvailable() -> some View {
if #available(macOS 26.0, *) {
scrollEdgeEffectStyle(.soft, for: .all)
} else {
self
}
}
}
//
// SettingsWindowController.swift
// {{AppName}}
//
// A singleton NSWindowController that creates the settings window with
// .fullSizeContentView for liquid glass rendering on macOS 26.
//
// Usage:
// SettingsWindowController.show(tab: .general)
//
import AppKit
import SwiftUI
@MainActor
final class SettingsWindowController: NSWindowController, NSWindowDelegate {
private static var shared: SettingsWindowController?
/// Show the settings window, optionally jumping to a specific tab.
static func show(tab: SettingsTab? = nil) {
if let tab {
SettingsNavigation.shared.selectedTab = tab
}
if shared == nil {
shared = SettingsWindowController()
}
shared?.showWindow(nil)
}
private init() {
let window = NSWindow(
contentRect: NSRect(origin: .zero, size: CGSize(width: 700, height: 540)),
styleMask: [
.titled,
.closable,
.resizable,
.miniaturizable,
.fullSizeContentView, // Required for liquid glass rounded corners
],
backing: .buffered,
defer: false
)
super.init(window: window)
configureWindow()
}
@available(*, unavailable)
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func configureWindow() {
guard let window else { return }
window.title = "Settings"
window.titleVisibility = .visible
window.titlebarAppearsTransparent = false
window.toolbarStyle = .automatic
window.isMovableByWindowBackground = true
window.setFrameAutosaveName("SettingsWindow")
window.minSize = NSSize(width: 620, height: 460)
window.center()
window.delegate = self
let hostingController = NSHostingController(rootView: SettingsView())
window.contentViewController = hostingController
}
override func showWindow(_ sender: Any?) {
super.showWindow(sender)
window?.makeKeyAndOrderFront(nil)
// If your app is menu-bar-only (.accessory activation policy),
// call your activation policy manager here:
// AppActivationPolicy.enter()
NSApp.activate(ignoringOtherApps: true)
}
func windowWillClose(_ notification: Notification) {
// If your app is menu-bar-only, call leave here:
// AppActivationPolicy.leave()
Self.shared = nil
}
}