Module 5

AppKit and the platform

SwiftUI is the modern layer, but it's built on AppKit — the Objective-C framework that has run the Mac since NeXTSTEP in the late 1980s. Anything genuinely Mac-specific still lives there, which is why Spoke has more AppKit than a typical tutorial app.

The naming convention gives it away: NS prefixes stand for NeXTSTEP. That's not legacy cruft; it's a remarkably durable API.

Windows and panels

NSWindow is a window. NSPanel is a subclass for auxiliary windows — palettes, inspectors, HUDs. Spoke's overlay is a panel because panels can do something windows can't: appear without stealing focus.

let panel = NSPanel(
    contentRect: NSRect(x: 0, y: 0, width: 400, height: 60),
    styleMask: [.borderless, .nonactivatingPanel],
    backing: .buffered,
    defer: false
)

Each setting on that panel is load-bearing:

SettingWhat it does
.nonactivatingPanelShowing this window never activates our app. The critical one.
.borderlessNo title bar or chrome — SwiftUI draws everything
level = .statusBarFloats above normal windows, below system alerts
isOpaque = false + clear backgroundLets the SwiftUI material show through with rounded corners
collectionBehavior.canJoinAllSpaces and .fullScreenAuxiliary make it appear over full-screen apps and follow you across Spaces
ignoresMouseEvents = trueClicks pass straight through to the app underneath

Trap · orderFrontRegardless vs makeKeyAndOrderFront

makeKeyAndOrderFront shows the window and makes it key, activating your app. In a dictation app that's fatal: the text field the user was typing into stops being the active target, and the paste lands somewhere else or nowhere. Always orderFrontRegardless() for non-activating overlays. This is the single highest-consequence line in OverlayController.

Precise semantics, since this is widely misstated: .nonactivatingPanel means the panel can become key without activating the owning app — it exists so palettes can receive keystrokes while another app stays frontmost. It does not by itself make a window non-interactive. Spoke additionally sets ignoresMouseEvents = true and never makes the panel key, because the overlay is purely a display.

Bridging SwiftUI into AppKit: NSHostingView wraps a SwiftUI view so it can be an AppKit view. The reverse (NSViewRepresentable) wraps AppKit for use inside SwiftUI. You'll use both eventually; Spoke only needs the first.

Events and monitors

macOS delivers input as NSEvent objects. Normally your app only sees events aimed at it — which is a problem when you want a hotkey that works while another app is focused.

// Fires when OTHER apps are focused. Cannot consume the event.
NSEvent.addGlobalMonitorForEvents(matching: .flagsChanged) { event in … }

// Fires when OUR app is focused. Can modify or swallow the event.
NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { event in … ; return event }

You need both, or your hotkey mysteriously dies whenever your own settings window has focus. HotkeyMonitor installs both for this reason.

.flagsChanged fires when a modifier key changes state. It doesn't say whether the key went down or up, so you read the current flags to infer direction — that's the isHeld bookkeeping in HotkeyMonitor.

Key codes are hardware positions, not characters. Right Option is 61, left Option is 58, V is 9. They're layout-independent, which is what you want for a hotkey and confusing the first time you see a magic number.

Going the other direction — synthesising input — uses Core Graphics:

let keyDown = CGEvent(keyboardEventSource: source, virtualKey: 9, keyDown: true)
keyDown?.flags = .maskCommand
keyDown?.post(tap: .cgAnnotatedSessionEventTap)

That's a synthetic ⌘V, indistinguishable to the receiving app from a real keystroke. It requires Accessibility permission, and it's how Spoke gets text into apps that expose no other insertion point.

The Accessibility API

Built for screen readers, this API lets one app inspect and control another's UI. It's the most powerful and least pleasant API in this document — pure C, with manual reference counting and stringly-typed attributes.

let systemWide = AXUIElementCreateSystemWide()

var focusedRef: CFTypeRef?
AXUIElementCopyAttributeValue(
    systemWide,
    kAXFocusedUIElementAttribute as CFString,
    &focusedRef
)

The pattern is always: create an element reference, ask for an attribute by string name, get back an untyped CFTypeRef you must check and cast yourself. Every call can fail, and failure is normal — many apps simply don't implement these attributes.

Spoke uses it to find the text caret so the overlay can appear next to it: get the focused element, ask for its selected text range, ask for the screen bounds of that range. Terminals, Electron apps, and some web views return nothing, which is why there's a mouse-position fallback. Design for this API failing.

Coordinate systems

macOS has two coordinate systems and they disagree about which way is up.

SystemOriginY increasesUsed by
AppKitBottom-left of primary screenUpwardNSWindow, NSView, NSScreen
Core Graphics / AccessibilityTop-left of primary screenDownwardCGEvent, AXUIElement

So a caret rectangle from the Accessibility API must be flipped before you can position an AppKit window with it:

let primaryHeight = NSScreen.screens.first!.frame.maxY
let flippedY = primaryHeight - rect.origin.y - rect.height

Trap · multi-monitor flipping

Always flip against the primary screen's height, not the current screen's. Both coordinate systems are anchored to the primary display, so using a secondary screen's height puts your window on the wrong monitor — or entirely off-screen, where you'll assume the code never ran. Debugging this by adding print statements is a rite of passage; skip it by getting it right the first time.

Spoke never sends your voice, your text, or anything else off this Mac. Not a promise in a privacy policy — a property of the binary.