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:
| Setting | What it does |
|---|---|
.nonactivatingPanel | Showing this window never activates our app. The critical one. |
.borderless | No title bar or chrome — SwiftUI draws everything |
level = .statusBar | Floats above normal windows, below system alerts |
isOpaque = false + clear background | Lets 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 = true | Clicks 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.
| System | Origin | Y increases | Used by |
|---|---|---|---|
| AppKit | Bottom-left of primary screen | Upward | NSWindow, NSView, NSScreen |
| Core Graphics / Accessibility | Top-left of primary screen | Downward | CGEvent, 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.