macOS: Windows, Menus, Commands, Settings and Menu Bar Extras

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages.

A Mac app lives in a different environment from its iPad sibling. People keep it open for hours next to a dozen other apps, drive it with a keyboard and a precise pointer, expect every command to be in the menu bar, arrange its windows across several displays, and expect it to come back exactly as they left it after a restart. This page turns FieldNotes' Mac build into that kind of app, targeting the baseline of this section: iOS, iPadOS, macOS, watchOS and visionOS 27, Xcode 27 and Swift 6.4, with a deployment minimum of 26. macOS 27 is Golden Gate; macOS 26, still in FieldNotes' supported range, is Tahoe. APIs that exist only on macOS 27 are labelled and guarded with if #available.

FieldNotes' Mac app is the native SwiftUI build of the same multiplatform target described on One Codebase for iOS, iPadOS, macOS, watchOS and visionOS. The scene types themselves were introduced on App Structure and Life Cycle (SwiftUI, UIKit, AppKit), and the menu bar commands and per-note windows were built for iPad on iPadOS: Windows, Multitasking, Pointer and Apple Pencil. This page reuses FieldNotesCommands, the selectedNote focused value and NoteWindow from there unchanged, and adds what only the Mac has: utility windows, window placement and levels, the Settings window, the menu bar extra, AppKit interoperability and the App Sandbox.

The FieldNotes Mac App at a Glance

On the Mac, FieldNotes declares seven scenes. Two are window groups that can have many windows, two are single windows, one is the Settings window and one lives in the system menu bar. The commands attached to the main window group build the menu bar for all of them.

Figure: The FieldNotes Mac scene graph

flowchart TB App["FieldNotesApp
store = AppServices.shared.store"] App --> Main["WindowGroup (main)
ContentView: notebooks, notes, detail"] App --> NoteWG["WindowGroup id note, for Note.ID
NoteWindow"] App --> Browser["Window id tag-browser
TagBrowser"] App --> Info["UtilityWindow id note-info
NoteInfoPanel"] App --> Inspector["UtilityWindow id tag-inspector
TagInspector"] App --> Settings["Settings
SettingsView: General and Sync tabs"] App --> Extra["MenuBarExtra FieldNotes
QuickCaptureMenu, menu or window style"] Main -->|".commands"| Cmds["FieldNotesCommands, SidebarCommands,
InspectorCommands, Help"] Cmds -.->|"reads focused value selectedNote"| NoteWG Cmds -.->|"reads focused value selectedNote"| Main Info -.->|"follows the focused main window"| Main Inspector -.->|"follows the focused main window"| NoteWG Extra -->|"openWindow(id: note, value:)"| NoteWG

Every scene that shows views injects the one store, so a pin toggled in a note window is visible in the main window, the utility windows and the menu bar extra at once:

// FieldNotes/FieldNotesApp.swift (excerpt: the macOS scenes; the other platforms are on the pages linked above)
import SwiftUI
import FieldNotesModel
import FieldNotesServices
import FieldNotesUI

@main
struct FieldNotesApp: App {
    @State private var store = AppServices.shared.store   // the single owner, see Background Execution
    #if os(macOS)
    @NSApplicationDelegateAdaptor private var appDelegate: FieldNotesMacDelegate
    @AppStorage("menuBarExtraChoice", store: .fieldNotesShared)
    private var menuBarExtraChoice = MenuBarExtraChoice.window
    #endif

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(store)
        }
        .commands {
            FieldNotesCommands(store: store)   // from the iPadOS page, unchanged
            SidebarCommands()
            InspectorCommands()
        }
        .defaultAppStorage(.fieldNotesShared)   // as on Preferences, Files and Document-Based Apps
        #if os(macOS)
        .defaultSize(width: 1100, height: 720)
        #endif

        WindowGroup(id: "note", for: Note.ID.self) { $noteID in
            NoteWindow(noteID: noteID)
                .environment(store)
        }
        .defaultAppStorage(.fieldNotesShared)
        .defaultSize(width: 640, height: 720)
        .windowResizability(.contentMinSize)

        #if os(macOS)
        Window("Tag Browser", id: "tag-browser") {
            TagBrowser()
                .environment(store)
        }
        .defaultAppStorage(.fieldNotesShared)
        .windowResizability(.contentSize)

        UtilityWindow("Note Info", id: "note-info") {
            NoteInfoPanel()
        }
        .defaultAppStorage(.fieldNotesShared)

        UtilityWindow("Tag Inspector", id: "tag-inspector") {
            TagInspector()
                .environment(store)
        }
        .defaultAppStorage(.fieldNotesShared)
        .defaultWindowPlacement { _, context in
            // Top-trailing corner of the screen the window opens on.
            let visible = context.defaultDisplay.visibleRect
            return WindowPlacement(CGPoint(x: visible.maxX - 300, y: visible.minY + 40))
        }
        .restorationBehavior(.disabled)

        Settings {
            SettingsView()
                .environment(store)
        }
        .defaultAppStorage(.fieldNotesShared)   // GeneralSettings writes the suite the App reads

        MenuBarExtra("FieldNotes", systemImage: "note.text", isInserted: isShown(.menu)) {
            QuickCaptureMenu()
                .environment(store)
        }
        .defaultAppStorage(.fieldNotesShared)
        .menuBarExtraStyle(.menu)

        MenuBarExtra("FieldNotes", systemImage: "note.text", isInserted: isShown(.window)) {
            VStack(alignment: .leading) {
                QuickCaptureBar()
                Divider()
                QuickCaptureMenu()
            }
            .frame(width: 300)
            .padding(.vertical, 8)
            .environment(store)
        }
        .defaultAppStorage(.fieldNotesShared)
        .menuBarExtraStyle(.window)
        #endif
    }

    #if os(macOS)
    /// At most one of the two menu bar extras is inserted; removing it from the menu bar hides both.
    private func isShown(_ choice: MenuBarExtraChoice) -> Binding<Bool> {
        Binding(get: { menuBarExtraChoice == choice },
                set: { if !$0 { menuBarExtraChoice = .hidden } })
    }
    #endif
}

Every scene keeps the .defaultAppStorage(.fieldNotesShared) it has on Preferences, Files and Document-Based Apps, so an @AppStorage without a store: argument anywhere in the app reads and writes the App Group suite. The App itself sits above every scene and inherits nothing, so its own menuBarExtraChoice names the suite explicitly; that is the same key GeneralSettings writes from the Settings scene, and the menu bar extra follows the picker at once.

The sections below take the scenes one at a time. FieldNotesMacDelegate, which keeps the app running after its last window closes and adds a Dock menu, is on App Structure and Life Cycle (SwiftUI, UIKit, AppKit). ContentView and NoteWindow publish the focused values the menu bar reads, as described on the iPadOS page.

Window Scenes: WindowGroup, Window and UtilityWindow

The three window scene types differ in how many windows they create and how those windows behave:

Scene Windows FieldNotes uses it for

WindowGroup

Any number, each with independent state. File > New Window (or FieldNotes' own commands) creates more. A data-presenting group (for:) has one window per value.

The main library window, and NoteWindow for one note per window.

Window

Exactly one. Opening it again brings the existing window forward. It appears in the Window menu under its title. If it is the app’s only scene, closing it quits the app.

Tag Browser, the library-wide list of tags.

UtilityWindow (macOS 15+)

One floating panel that stays above main windows, hides when the app is inactive, and is listed in the View menu with a show/hide command. Its content reads focused values from the main window in front.

Note Info and Tag Inspector, which describe whatever note is frontmost.

TagInspector shows every tag in the library with its note count, and highlights the tags of the note in the frontmost main or note window. It is read-only; editing tags happens in the note itself:

// FieldNotes/Mac/TagInspector.swift
#if os(macOS)
import SwiftUI
import FieldNotesModel
import FieldNotesServices
import FieldNotesUI

struct TagInspector: View {
    @Environment(NoteStore.self) private var store
    @FocusedValue(\.selectedNote) private var selectedNote   // declared in FieldNotesUI, set by NoteDetail

    /// Every tag in the library, with how many notes use it, most used first.
    private var tagCounts: [(tag: String, count: Int)] {
        Dictionary(grouping: store.notes.flatMap(\.tags), by: { $0 })
            .map { (tag: $0.key, count: $0.value.count) }
            .sorted { $0.count != $1.count ? $0.count > $1.count : $0.tag < $1.tag }
    }

    var body: some View {
        List(tagCounts, id: \.tag) { entry in
            HStack {
                TagChip(entry.tag, isSelected: selectedNote?.tags.contains(entry.tag) == true)
                Spacer()
                Text(entry.count, format: .number)
                    .foregroundStyle(.secondary)
                    .monospacedDigit()
            }
        }
        .navigationSubtitle(selectedNote?.title ?? String(localized: "No note selected"))
        .frame(minWidth: 240, minHeight: 300)
    }
}
#endif

The List uses the tag string as its identity, which is stable because tags are keys of the dictionary. The utility window works because NoteDetail publishes its note with focusedSceneValue(\.selectedNote, note): a utility window never becomes the focused scene itself, so it keeps seeing the value of the main window behind it.

openWindow and dismissWindow

Windows with an id are opened by the openWindow action and closed by dismissWindow. Called with no arguments, dismissWindow() closes the window the view is in; with an id it closes that scene’s window. NoteWindow closes itself after the person deletes its note, and a toolbar button in the main window shows the tag inspector:

// FieldNotes/Windows/NoteWindow+Delete.swift (excerpt: NoteWindow's toolbar gains a Delete button)
@Environment(\.dismissWindow) private var dismissWindow

Button("Delete Note", systemImage: "trash", role: .destructive) {
    store.delete(note)
    dismissWindow()          // this window has nothing left to show
}

// FieldNotes/Mac/TagInspectorToolbarButton.swift
struct TagInspectorToolbarButton: View {
    @Environment(\.openWindow) private var openWindow

    var body: some View {
        Button("Tag Inspector", systemImage: "tag.circle") {
            openWindow(id: "tag-inspector")
        }
    }
}

Sizing, Placement and Styles

A Mac window has a size, a position, a title bar and toolbar style, and a level. SwiftUI sets all four with scene modifiers, so the choice is declared once and applies to every window of the scene.

Size and resizability

defaultSize(width:height:) is the size of a new window; after that the person’s size wins and is restored. windowResizability(_:) decides how the window’s size relates to its content: .automatic (the default), .contentSize (the window is exactly as large as the content allows, between its minimum and maximum frames) and .contentMinSize (the content sets only the minimum). The Tag Browser uses .contentSize above, so its frame modifiers fully control it:

// FieldNotes/Mac/TagBrowser.swift (excerpt)
var body: some View {
    TagBrowserList()
        .frame(minWidth: 320, idealWidth: 380, maxWidth: 520,
               minHeight: 360, maxHeight: .infinity)   // width fixed within a range, height free
}

Placement and zoom

Since macOS 15, defaultWindowPlacement(_:) computes where a new window appears. The closure receives a WindowLayoutRoot proxy, whose sizeThatFits(_:) asks the content for its size, and a WindowPlacementContext with the display the window will open on. The Tag Inspector above opens in the top-trailing corner of the visible area of that display, clear of the menu bar and the Dock. windowIdealPlacement(_:) answers a different question: where the window goes when the person Option-clicks the green button to zoom it. FieldNotes' note windows zoom to a readable column rather than the full screen:

// FieldNotes/FieldNotesApp.swift (excerpt: the note window group on macOS)
WindowGroup(id: "note", for: Note.ID.self) { $noteID in
    NoteWindow(noteID: noteID)
        .environment(store)
}
.defaultAppStorage(.fieldNotesShared)   // as in the full scene list above
#if os(macOS)
.windowIdealPlacement { root, context in
    let display = context.defaultDisplay.visibleRect
    let ideal = root.sizeThatFits(.unspecified)
    let size = CGSize(width: min(max(ideal.width, 720), display.width),   // a comfortable line length
                      height: display.height)
    return WindowPlacement(CGPoint(x: display.midX - size.width / 2, y: display.minY), size: size)
}
#endif

Title bar, toolbar and level

Window style covers the frame and title bar; toolbar style covers the unified toolbar; a few view modifiers remove parts of it. The Note Info panel gets a compact toolbar, the Tag Browser floats above other windows without a title bar, and a note window in focus mode hides its title so the text reaches the top edge:

// FieldNotes/FieldNotesApp.swift (excerpt: styles on the Mac scenes)
UtilityWindow("Note Info", id: "note-info") {
    NoteInfoPanel()
}
.defaultAppStorage(.fieldNotesShared)
.windowToolbarStyle(.unifiedCompact)

Window("Tag Browser", id: "tag-browser") {
    TagBrowser()
        .environment(store)
}
.defaultAppStorage(.fieldNotesShared)
.windowLevel(.floating)          // stays above other apps' windows while FieldNotes is active
.windowStyle(.hiddenTitleBar)    // no title text or title bar backing

// Inside NoteWindow, when focus mode is on:
NoteDetail(note: note)
    .toolbar(removing: .title)
    .toolbarBackgroundVisibility(.hidden, for: .windowToolbar)
    .gesture(WindowDragGesture())   // the window stays draggable by its content

Removing the title and the toolbar background is a visual change only: VoiceOver still reads the title, and the Window menu still lists it, which is why every FieldNotes window keeps a navigationTitle. Without a toolbar background the window has no obvious place to drag it, so WindowDragGesture makes the content itself a drag handle. Use .floating sparingly: it suits a small reference panel, not a document window.

Restoration and launch behavior

macOS restores windows at launch when the person has Close windows when quitting an app turned off in System Settings, which is the default. SwiftUI follows that setting, restoring each window’s frame, its presented value (the Note.ID of a note window) and its @SceneStorage. Two scene modifiers adjust it per scene:

  • restorationBehavior(.disabled) opts a scene’s windows out of restoration. The Tag Inspector uses it above, because it describes the current selection and reopening it at launch, before any note is selected, is noise.

  • defaultLaunchBehavior(_:) decides whether a scene presents a window at launch when nothing was restored: .presented opens it, .suppressed keeps it closed.

// FieldNotes/FieldNotesApp.swift (excerpt): the tag browser never opens by itself at launch,
// but it is restored if it was open when the person quit.
Window("Tag Browser", id: "tag-browser") {
    TagBrowser()
        .environment(store)
}
.defaultAppStorage(.fieldNotesShared)
.defaultLaunchBehavior(.suppressed)

What is worth restoring inside a window is UI state, such as the selected notebook in @SceneStorage, never the data itself; @SceneStorage is covered on the life-cycle page.

Menus and Commands

The Mac menu bar is the app’s primary command surface: every action a person can take should be reachable there, with a keyboard shortcut for the frequent ones. FieldNotes gets most of its menu bar from FieldNotesCommands, defined on iPadOS: Windows, Multitasking, Pointer and Apple Pencil and attached to the main window group above. It replaces the File > New group with New Note (⌘N) and New Window for Note (⌥⌘N), removes Print so that ⌘P can pin, and adds a Notes menu with Pin Note and Find Notes… (⌘F), all driven by @FocusedValue(\.selectedNote) and two focused bindings from ContentView.

The Mac adds a few commands of its own, in a second Commands type that only the macOS build includes: a Window menu item for each utility window with a shortcut, Export Notebook… in the File menu, and the help link from Human Interface Guidelines in Practice:

// FieldNotes/Mac/MacOnlyCommands.swift
#if os(macOS)
import SwiftUI
import FieldNotesModel
import FieldNotesServices

struct MacOnlyCommands: Commands {
    let store: NoteStore

    @Environment(\.openWindow) private var openWindow
    @FocusedValue(\.selectedNote) private var selectedNote

    /// The notebook of the frontmost note, if it has one.
    private var notebook: Notebook? {
        store.notebooks.first { $0.id == selectedNote?.notebookID }
    }

    var body: some Commands {
        CommandGroup(after: .importExport) {
            Button("Export Notebook…") {
                guard let notebook else { return }
                do {
                    try exportNotebook(notebook, from: store)   // shown under App Sandbox below
                } catch {
                    NSAlert(error: error).runModal()            // never fail silently
                }
            }
            .keyboardShortcut("e", modifiers: [.command, .shift])
            .disabled(notebook == nil)
        }

        CommandGroup(before: .windowList) {
            Button("Tag Browser") { openWindow(id: "tag-browser") }
                .keyboardShortcut("t", modifiers: [.command, .option])
            Button("Tag Inspector") { openWindow(id: "tag-inspector") }
                .keyboardShortcut("i", modifiers: [.command, .option])
            Divider()
        }

        CommandGroup(replacing: .help) {
            Link("FieldNotes Help", destination: URL(string: "https://example.com/fieldnotes/help")!)
        }
    }
}
#endif

The scene’s commands closure then lists both: FieldNotesCommands(store: store) everywhere and MacOnlyCommands(store: store) inside #if os(macOS). A few rules keep a Mac menu bar trustworthy:

  • Disable, don’t hide. A command that can’t run right now stays in its menu, greyed out, so people learn where it lives. Every FieldNotes command uses .disabled(_:) with its focused value.

  • Scene-wide or view-focused. focusedSceneValue (what NoteDetail uses) keeps a command enabled whatever has keyboard focus in the window; focusedValue(_:_:) only while focus is inside the modified view, which suits commands like Delete Row in a table.

  • Standard placements first. CommandGroupPlacement names every standard group (.newItem, .saveItem, .importExport, .printItem, .pasteboard, .textEditing, .sidebar, .toolbar, .windowList, .help and more). Putting Export next to Import in the File menu, rather than in a custom menu, is what Mac users expect.

  • Symbols are hidden by default in 27. As on iPadOS 27, menu item symbol images are not shown in macOS 27 menus and context menus unless a Label uses .titleAndIcon; see the iPadOS page for the example.

The AppKit machinery beneath commands (an NSMenu whose items send actions up the responder chain) is on UIKit and AppKit Essentials, and Mixing Them with SwiftUI.

The Settings scene gives the app the standard FieldNotes > Settings… menu item and the ⌘, shortcut, and opens one Settings window. Mac settings windows are traditionally organized in panes with a toolbar of tabs. A TabView inside the Settings scene produces exactly that, so SettingsView gains tabs on macOS while the iOS sheet keeps one grouped form. The sections that used to be inline in its Form (Controls, Forms, Text, Images and SF Symbols) move into two views, GeneralSettings and SyncSettings:

// Packages/FieldNotesUI/Sources/FieldNotesUI/SettingsView.swift (updated: tabs on macOS)
import SwiftUI

public struct SettingsView: View {
    public init() {}

    public var body: some View {
        #if os(macOS)
        TabView {
            Tab("General", systemImage: "gearshape") {
                Form { GeneralSettings() }.formStyle(.grouped)
            }
            Tab("Sync", systemImage: "arrow.triangle.2.circlepath.icloud") {
                Form { SyncSettings() }.formStyle(.grouped)
            }
        }
        .frame(width: 520, height: 460)
        #else
        Form {
            SyncSettings()
            GeneralSettings()
        }
        .formStyle(.grouped)
        .navigationTitle("Settings")
        #endif
    }
}

GeneralSettings holds the Notes, Capture and About sections from the original form, unchanged, and on the Mac one more: the menu bar extra choice. SyncSettings holds the iCloud toggle and, on the Mac, the export folder described under The App Sandbox and ExportFolderBookmark:

// Packages/FieldNotesUI/Sources/FieldNotesUI/GeneralSettings.swift
import SwiftUI
import FieldNotesModel
import FieldNotesServices

/// Where the Mac app shows its menu bar extra, stored in the App Group suite.
public enum MenuBarExtraChoice: String, CaseIterable, Identifiable, Sendable {
    case hidden, menu, window
    public var id: Self { self }
}

public struct GeneralSettings: View {
    @Environment(NoteStore.self) private var store
    @AppStorage("pinnedFirst") private var pinnedFirst = true
    @AppStorage("noteSortOrder") private var sortOrder = NoteSortOrder.newestFirst
    #if os(macOS)
    @AppStorage("menuBarExtraChoice") private var menuBarExtraChoice = MenuBarExtraChoice.window
    #endif

    public init() {}

    public var body: some View {
        Section("Notes") {
            Picker("Sort By", selection: $sortOrder) {
                ForEach(NoteSortOrder.allCases) { Text($0.title).tag($0) }
            }
            Toggle("Show Pinned Notes First", isOn: $pinnedFirst)
            // Default Notebook picker as before
        }
        #if os(macOS)
        Section("Menu Bar") {
            Picker("Quick Capture", selection: $menuBarExtraChoice) {
                Text("Hidden").tag(MenuBarExtraChoice.hidden)
                Text("Menu").tag(MenuBarExtraChoice.menu)
                Text("Panel").tag(MenuBarExtraChoice.window)
            }
        }
        #endif
        // Capture and About sections as before
    }
}

// Packages/FieldNotesUI/Sources/FieldNotesUI/SyncSettings.swift
import SwiftUI

public struct SyncSettings: View {
    @AppStorage("syncWithICloud") private var syncWithICloud = true

    public init() {}

    public var body: some View {
        Section("Sync") {
            Toggle("Sync with iCloud", isOn: $syncWithICloud)
        }
        #if os(macOS)
        Section("Export") {
            ExportFolderRow()   // also in FieldNotesUI, shown under App Sandbox below
        }
        #endif
    }
}

The @AppStorage properties don’t name a store because the Settings scene’s content (and, on iOS, the main window group that presents the settings sheet) inherits .defaultAppStorage(.fieldNotesShared), as set up on Preferences, Files and Document-Based Apps and kept on every scene in FieldNotesApp above; the App names the suite explicitly with store: .fieldNotesShared because it sits above every scene. Both sides therefore read and write the same App Group suite. The ForEach over NoteSortOrder.allCases uses the enum’s own stable identity.

To open Settings from inside the app, for example from the menu bar extra or from an "iCloud is off" banner, use SettingsLink, a button that opens (or brings forward) the Settings window. Its default label is the system’s localized Settings…. The openSettings environment action does the same from code:

// FieldNotes/Mac/SyncOffBanner.swift
#if os(macOS)
import SwiftUI

struct SyncOffBanner: View {
    @Environment(\.openSettings) private var openSettings

    var body: some View {
        HStack {
            Label("iCloud sync is off", systemImage: "icloud.slash")
            Spacer()
            SettingsLink {
                Text("Open Settings…")
            }
            Button("Turn On in Settings") {
                openSettings()   // the same as SettingsLink, for use inside another action
            }
        }
        .padding(8)
    }
}
#endif

SettingsLink and openSettings are the SwiftUI routes to the Settings window. On iPad the same Settings menu item opens FieldNotes' page in the Settings app instead.

Menu Bar Extras

A MenuBarExtra puts an item in the right-hand side of the system menu bar, available even when the app has no windows open. It has two styles:

  • .menu (the default) shows a pull-down menu. The content is interpreted as menu items: Button, Toggle, Picker, Menu for submenus, Divider and Section. Layout containers and text fields have no meaning in a menu.

  • .window shows a small popover-like window with arbitrary SwiftUI content, which suits a text field.

FieldNotes lets people choose between them, or hide the item, with the Menu Bar picker in General settings. The app declares both scenes and inserts at most one, through the isInserted: bindings shown in FieldNotesApp. If the person Command-drags the item out of the menu bar, the system sets the binding to false and the choice becomes Hidden. QuickCaptureMenu is written only with menu-compatible views, so it works in both styles:

// FieldNotes/Mac/QuickCaptureMenu.swift
#if os(macOS)
import SwiftUI
import FieldNotesModel
import FieldNotesServices

/// The menu bar extra's content: menu items in .menu style, the lower part of the panel in .window style.
struct QuickCaptureMenu: View {
    @Environment(NoteStore.self) private var store
    @Environment(\.openWindow) private var openWindow

    private var pinned: [Note] {
        store.notes.filter(\.isPinned).sorted { $0.createdAt > $1.createdAt }
    }

    var body: some View {
        Button("New Note from Clipboard") {
            let text = NSPasteboard.general.string(forType: .string) ?? ""
            let note = Note(title: String(text.prefix(60)), body: text)
            store.add(note)
            openWindow(id: "note", value: note.id)
            NSApplication.shared.activate()
        }
        .disabled(NSPasteboard.general.string(forType: .string) == nil)

        Section("Pinned") {
            ForEach(pinned.prefix(5)) { note in
                Button(note.title) {
                    openWindow(id: "note", value: note.id)
                    NSApplication.shared.activate()
                }
            }
        }

        Divider()
        SettingsLink()
        Button("Quit FieldNotes") { NSApplication.shared.terminate(nil) }
            .keyboardShortcut("q")
    }
}
#endif

In .window style, FieldNotesApp wraps the same content in a VStack below QuickCaptureBar, the capsule text field from Controls, Forms, Text, Images and SF Symbols, so a thought can be captured without opening any window. ForEach(pinned.prefix(5)) identifies rows by Note.ID. Calling NSApplication.shared.activate() after opening a window brings FieldNotes forward; without it the new window can open behind the app the person is using. Note(title:body:) goes through store.add(_:), so the new note is written to SwiftData like any other.

An app whose only scene is a MenuBarExtra is a menu bar utility: it usually sets LSUIElement in Info.plist to hide its Dock icon, and it quits when its item is removed. FieldNotes is a regular app with an optional extra, so it keeps its Dock icon and, through FieldNotesMacDelegate, keeps running when its last window closes.

AppKit in macOS 27

SwiftUI builds FieldNotes' Mac interface, but AppKit is still underneath it, and many Mac apps, including FieldNotes' predecessor FieldNotesMacLegacy from the life-cycle page, are AppKit apps. WWDC26’s Modernize your AppKit app session collects what changed for them in macOS 27: modern event handling with gesture recognizers and control events instead of mouseDown tracking loops, better keyboard navigation (including for status items), graceful termination and state restoration, and Liquid Glass and corner-concentricity updates. Two changes matter most for code that shares a model with SwiftUI.

Observation tracking in AppKit

AppKit tracks @Observable models automatically: any observable property read inside updateConstraints(), layout(), draw(_:), or a view controller’s viewWillLayout() is recorded, and AppKit reruns the method when that property changes. It became available in macOS 15, where it had to be enabled with the NSObservationTrackingEnabled Info.plist key; since macOS 26 it is on by default, so FieldNotes' deployment minimum gets it with no configuration. NoteStore is @Observable, so an AppKit view that reads it updates itself:

// FieldNotesMacLegacy/NoteCountBadgeView.swift
import AppKit
import FieldNotesServices

/// A Dock-tile-style badge showing the number of pinned notes. No notifications, no KVO, no manual invalidation.
final class NoteCountBadgeView: NSView {
    private let store: NoteStore

    init(store: NoteStore) {
        self.store = store
        super.init(frame: NSRect(x: 0, y: 0, width: 44, height: 24))
    }

    required init?(coder: NSCoder) { fatalError("init(coder:) is not used") }

    override func draw(_ dirtyRect: NSRect) {
        // Reading store.notes here registers a dependency; a pin anywhere in the app redraws the badge.
        let count = store.notes.filter(\.isPinned).count
        NSColor.controlAccentColor.setFill()
        NSBezierPath(roundedRect: bounds, xRadius: bounds.height / 2, yRadius: bounds.height / 2).fill()
        let text = count.formatted() as NSString
        let attributes: [NSAttributedString.Key: Any] = [
            .font: NSFont.monospacedDigitSystemFont(ofSize: 13, weight: .semibold),
            .foregroundColor: NSColor.white,
        ]
        let size = text.size(withAttributes: attributes)
        text.draw(at: NSPoint(x: bounds.midX - size.width / 2, y: bounds.midY - size.height / 2),
                  withAttributes: attributes)
    }
}

NSView subclasses are main-actor isolated, like NoteStore, so the read in draw(_:) needs no await. The same rule applies as in SwiftUI: only properties actually read during the tracked method are dependencies, so read what you draw and nothing more.

Control events

UIKit has always had control events: a target and action registered for .valueChanged or .touchUpInside. AppKit controls traditionally sent one action, and anything finer meant subclassing and overriding mouseDown. The WWDC26 session introduces NSControl.Events and addTarget(_:action:for:): tracking events (.trackingBegan, .trackingEndedInside, .trackingEndedOutside, .trackingCancelled and more) and semantic events (.valueChanged, .primaryActionTriggered, .menuActionTriggered), registered on standard controls without subclassing. FieldNotesMacLegacy’s map zoom slider previews while dragging and commits once, when the drag ends:

// FieldNotesMacLegacy/MapZoomController.swift
import AppKit

final class MapZoomController: NSViewController {
    private let slider = NSSlider(value: 1, minValue: 0.5, maxValue: 4, target: nil, action: nil)

    override func viewDidLoad() {
        super.viewDidLoad()
        slider.isContinuous = true
        slider.addTarget(self, action: #selector(previewZoom(_:)), for: .valueChanged)
        slider.addTarget(self, action: #selector(commitZoom(_:)), for: [.trackingEndedInside, .trackingEndedOutside])
        view.addSubview(slider)
    }

    @objc private func previewZoom(_ sender: NSSlider) {
        // cheap: scale the current map snapshot
    }

    @objc private func commitZoom(_ sender: NSSlider) {
        // expensive: request new map tiles for the final zoom level
    }
}

The documentation lists NSControl.Events and addTarget(_:action:for:) as available from macOS 11, and the session explains that most of the events work on releases long before 27, so the code above needs no availability check at FieldNotes' deployment minimum of 26. The session also covers text selection in custom views, keyboard navigation of status items and NSWindow.preventsApplicationTerminationWhenModal, which lets a non-essential sheet stop blocking Quit:

// FieldNotesMacLegacy/ExportSheet.swift (excerpt)
exportSheetWindow.preventsApplicationTerminationWhenModal = false   // quitting just cancels the export
mainWindow.beginSheet(exportSheetWindow)

Hosting SwiftUI inside AppKit and AppKit inside SwiftUI (NSHostingController, NSHostingView, NSViewRepresentable) is covered on UIKit and AppKit Essentials, and Mixing Them with SwiftUI.

The App Sandbox and ExportFolderBookmark

A Mac App Store app must be sandboxed, and FieldNotes is sandboxed everywhere. The sandbox confines the app to its container in ~/Library/Containers, and every other resource (network, location, user files) requires an entitlement. The Mac entitlements file, with com.apple.security.files.user-selected.read-write, is on Security and Privacy. That entitlement lets the app read and write what the person picks in an open or save panel, during that session.

FieldNotes exports notebooks as .fieldnotes files to a folder the person chooses once, in Sync settings. The access granted by the panel ends when the app quits, so to keep writing to that folder on later launches the app stores a security-scoped bookmark: opaque data, created while it has access, that it can resolve later to regain access. ExportFolderBookmark wraps the whole protocol:

// Packages/FieldNotesKit/Sources/FieldNotesServices/Mac/ExportFolderBookmark.swift
#if os(macOS)
import Foundation

/// The export folder the person chose, remembered across launches as a security-scoped bookmark
/// in `UserDefaults.fieldNotesShared`.
public enum ExportFolderBookmark {
    public enum AccessError: Error {
        case noFolderChosen
        case accessDenied(URL)
    }

    private static let key = "exportFolderBookmark"

    /// Call with a URL from fileImporter or NSOpenPanel, while the panel's access is still active.
    public static func remember(_ folder: URL) throws {
        let data = try folder.bookmarkData(options: .withSecurityScope,
                                           includingResourceValuesForKeys: nil, relativeTo: nil)
        UserDefaults.fieldNotesShared.set(data, forKey: key)
    }

    public static var hasFolder: Bool {
        UserDefaults.fieldNotesShared.data(forKey: key) != nil
    }

    /// Resolves the bookmark, opens the sandbox for the folder, runs `body`, and closes it again.
    public static func withAccess<T>(_ body: (URL) throws -> T) throws -> T {
        guard let data = UserDefaults.fieldNotesShared.data(forKey: key) else {
            throw AccessError.noFolderChosen
        }
        var isStale = false
        let folder = try URL(resolvingBookmarkData: data, options: .withSecurityScope,
                             relativeTo: nil, bookmarkDataIsStale: &isStale)
        guard folder.startAccessingSecurityScopedResource() else {
            throw AccessError.accessDenied(folder)
        }
        defer { folder.stopAccessingSecurityScopedResource() }
        if isStale {
            try remember(folder)   // the folder moved or was renamed: refresh the stored bookmark
        }
        return try body(folder)
    }
}
#endif

The order of operations is what Apple’s sandbox article prescribes: resolve with .withSecurityScope, start access, recreate the bookmark if it is stale, use the files, stop access. Every start is balanced by a stop through defer, because the kernel keeps a limited number of open sandbox extensions per process. The bookmark is only resolvable by the app that created it, so storing it in the App Group suite doesn’t give the widget access to the folder; it just keeps FieldNotes' preferences in one place.

The settings row chooses the folder with fileImporter, and the export writes through withAccess. Errors reach the person as an alert instead of disappearing. The row lives in the FieldNotesUI package next to SyncSettings, which shows it: a package can’t see types declared in the app target, but FieldNotesUI already depends on FieldNotesServices, where ExportFolderBookmark is declared. The export function, called only by the app’s MacOnlyCommands, stays in the app target:

// Packages/FieldNotesUI/Sources/FieldNotesUI/Mac/ExportFolderRow.swift
#if os(macOS)
import SwiftUI
import UniformTypeIdentifiers
import FieldNotesServices

public struct ExportFolderRow: View {
    @State private var isChoosing = false
    @State private var hasFolder = ExportFolderBookmark.hasFolder
    @State private var errorMessage: String?

    public init() {}

    public var body: some View {
        LabeledContent("Export Folder") {
            Button(hasFolder ? "Change…" : "Choose…") { isChoosing = true }
        }
        .fileImporter(isPresented: $isChoosing, allowedContentTypes: [.folder]) { result in
            do {
                try ExportFolderBookmark.remember(try result.get())
                hasFolder = true
            } catch {
                errorMessage = error.localizedDescription
            }
        }
        .alert("Couldn't Use That Folder", isPresented: Binding(
            get: { errorMessage != nil }, set: { if !$0 { errorMessage = nil } })) {
            Button("OK") {}
        } message: {
            Text(errorMessage ?? "")
        }
    }
}
#endif

// FieldNotes/Mac/ExportNotebook.swift (app target: the export itself, called from the Export Notebook command)
#if os(macOS)
import Foundation
import FieldNotesModel
import FieldNotesServices

@MainActor
func exportNotebook(_ notebook: Notebook, from store: NoteStore) throws {
    let notes = store.notes.filter { $0.notebookID == notebook.id }
    let envelope = NoteFileFormat.Envelope(notebookName: notebook.name, notes: notes)
    let data = try JSONEncoder().encode(envelope)
    try ExportFolderBookmark.withAccess { folder in
        try data.write(to: folder.appending(path: "\(notebook.name).fieldnotes"), options: .atomic)
    }
}
#endif

The URL that fileImporter returns is already security-scoped for this session (the system started access as if the app had called startAccessingSecurityScopedResource()), which is why remember(_:) can create the bookmark directly. NoteFileFormat.Envelope is the .fieldnotes format from Preferences, Files and Document-Based Apps. The App Sandbox capability and the user-selected-files entitlement need a signing team; FieldNotes' entitlements are assembled on Membership, Signing, Capabilities and Info.plist.

The sandbox is required only for the Mac App Store, but FieldNotes keeps it in every build, including a Developer ID build distributed from its website. That build loses a few capabilities regardless of the sandbox: Apple’s Supported capabilities (macOS) reference lists Sign in with Apple, In-App Purchase, Game Center and WeatherKit as available to App Store apps only, not to Developer ID apps, so FieldNotes' Developer ID build hides Sign in with Apple and its StoreKit upgrade. Signing, notarizing and shipping that build is on Distributing Mac Apps with Developer ID and Notarization.

Apple Silicon Only, and the End of Rosetta

The Mac hardware baseline changes with this release, and the dates matter for FieldNotes' deployment minimum:

Release What it means for a Mac app

macOS 26 Tahoe (2025)

The last macOS release for Intel-based Macs. FieldNotes supports it, so its Release build stays a universal binary (arm64 and x86_64) for Intel Macs that remain on Tahoe.

macOS 27 Golden Gate (2026)

Runs only on Macs with Apple silicon. Rosetta is still included as a general-purpose translator for Intel apps, for the last time. Settings > General lists installed Intel apps that won’t run on macOS 28, and Get Info marks them.

macOS 28 (expected 2027)

Rosetta remains only as a subset for older, unmaintained games that depend on Intel frameworks; other Intel-only apps stop running.

Xcode’s Standard Architectures setting builds both slices for Release, so a SwiftUI app like FieldNotes is universal without extra work. Check the shipped binary, and any third-party binary framework inside it, before dropping x86_64:

# Which architectures does the archived Mac app contain?
lipo -archs "FieldNotes.app/Contents/MacOS/FieldNotes"
# arm64 x86_64

# Any Intel-only framework left in the bundle would stop working on macOS 28.
find FieldNotes.app/Contents/Frameworks -type f -perm +111 -exec lipo -archs {} \; 2>/dev/null

When FieldNotes raises its minimum to macOS 27, the x86_64 slice can go, since no Intel Mac can install that release.

Native SwiftUI, Mac Catalyst, Designed for iPad or AppKit?

There are four ways to put an app on the Mac. One Codebase for iOS, iPadOS, macOS, watchOS and visionOS compares the three that start from an iPad app (native, Mac Catalyst, Designed for iPad) in a table; this flowchart adds AppKit and turns the comparison into a decision:

Figure: Choosing how to build the Mac app

flowchart TD Start([You want a Mac app]) --> Existing{"Is there an existing app?"} Existing -->|No, new app| NewUI{"Needs deep Mac-only behavior,
e.g. a complex text or canvas engine?"} NewUI -->|No| Native["Native SwiftUI, Mac destination
(FieldNotes)"] NewUI -->|Yes| NativeMix["Native SwiftUI plus AppKit
through NSViewRepresentable"] Existing -->|Mac AppKit app| AppKit["Stay on AppKit, modernize for macOS 27;
add SwiftUI views with NSHostingView"] Existing -->|iPad app| Tech{"Built with SwiftUI or UIKit?"} Tech -->|SwiftUI| Native Tech -->|UIKit| Invest{"Willing to adapt layouts, menus
and windows for the Mac?"} Invest -->|Yes| Catalyst["Mac Catalyst
(Optimize Interface for Mac)"] Invest -->|No| DFI["Designed for iPad
(unmodified iPad binary, Apple silicon only)"] DFI -. later .-> Catalyst Catalyst -. rewrite screens over time .-> Native

In code, the choice shows up as the conditions a shared file is compiled or run under. FieldNotes is native, so its Mac-specific code uses #if os(macOS). A Catalyst build is os(iOS) with targetEnvironment(macCatalyst), and a Designed-for-iPad build is the iPad binary itself, detectable only at run time:

// Packages/FieldNotesUI/Sources/FieldNotesUI/PlatformDescription.swift
import Foundation

/// How this build of FieldNotes is running on a Mac, if at all (shown in the About section).
func macRuntimeDescription() -> String? {
    #if os(macOS)
    return "Native Mac app"                                   // FieldNotes' Mac destination: AppKit and SwiftUI
    #elseif targetEnvironment(macCatalyst)
    return "Mac Catalyst"                                     // UIKit compiled for the Mac
    #elseif os(iOS)
    return ProcessInfo.processInfo.isiOSAppOnMac ? "iPad app on Mac" : nil   // Designed for iPad
    #else
    return nil
    #endif
}

For FieldNotes the answer was never in doubt: a SwiftUI app gets the best Mac result from the native destination, and everything on this page (utility windows, window levels, Settings, MenuBarExtra, the sandboxed bookmark) is native macOS API that a Designed-for-iPad build can’t use at all. Mac Catalyst remains the right path for a large UIKit iPad app, and AppKit remains the right foundation for Mac apps that already have years of AppKit code. Both UIKit-on-Mac routes require Apple silicon on macOS 27 and later, like everything else on the Mac. Distributing the native build outside the Mac App Store is on Distributing Mac Apps with Developer ID and Notarization.

Availability and Requirements

Platform availability

API or feature iOS iPadOS macOS watchOS visionOS

WindowGroup, openWindow, dismissWindow

Yes (one window)

Yes

Yes

Yes (one window, no openWindow)

Yes

Window

Not available

Not available

Yes (13)

Not available

New in 26

UtilityWindow

Not available

Not available

Yes (15)

Not available

Not available

defaultWindowPlacement(_:)

Not available

Not available

Yes (15)

Not available

Yes (2)

windowIdealPlacement(_:), windowLevel(_:), WindowDragGesture

Not available

Not available

Yes (15)

Not available

Not available

restorationBehavior(_:), defaultLaunchBehavior(_:)

Yes (18) / Not available

Yes (18) / Not available

Yes (15)

Yes (11) / Not available

Yes (2) / New in 26

commands, CommandMenu, CommandGroup

Yes (shortcuts only)

Yes (menu bar in 26)

Yes

Not available

Yes

Settings, SettingsLink, openSettings

Not available

Not available (Settings app instead)

Yes (SettingsLink 14)

Not available

Not available

MenuBarExtra

Not available

Not available

Yes (13)

Not available

Not available

AppKit Observation tracking

Not applicable

Not applicable

Yes (15, opt-in; on by default in 26)

Not applicable

Not applicable

NSControl.Events, addTarget(_:action:for:)

Not applicable

Not applicable

Yes (listed from 11, documented in the 27 SDK)

Not applicable

Not applicable

App Sandbox, security-scoped bookmarks

Always sandboxed

Always sandboxed

Entitlement

Always sandboxed

Always sandboxed

The App Sandbox capability and its file-access entitlements need a signing team in Xcode; Mac App Store distribution needs a paid Apple Developer Program membership. Everything else on this page runs on any Apple silicon Mac with macOS 26 or later, and the macOS 27 behaviors need macOS 27.

References