Make the app reliable
Complete a native macOS utility
Finish a small app with a sidebar, durable data, menus, settings, one system integration, and tests for its important behavior.
12 minute lesson
You have all the pieces. The last lesson is assembly: one small utility, built end to end, that behaves like it belongs on a Mac.
Pick a scope you can finish. The notes app we have been building is one option. A command runner — saved shell tasks with captured output — is another that exercises the process material. My advice is to choose the one you would actually use, because you will test it harder.
The requirements map to what you built across the course:
- A
WindowGroupmain window that resizes sanely, plus any purpose-builtWindowthe app needs - One model owning the data, injected at the app level, with views that render state and send actions
- A
NavigationSplitViewsidebar, or a deliberate single-pane layout if the app is truly one list - Menu commands with keyboard shortcuts, disabled when they do not apply
- A
Settingsscene with@AppStoragepreferences - Durable data:
Codablemodels, atomic writes, Application Support
Then one — exactly one — system integration from the previous modules: a menu bar extra, imported files with bookmarks, notifications, a Keychain item, or a child process. Pick the one your app’s workflow genuinely needs. Four half-wired integrations impress nobody. One that works every time is what makes the app feel finished.
Write tests where they pay. Model logic first:
@MainActor
func testCreateNoteAppendsToList() {
let model = NotesModel()
model.createNote()
XCTAssertEqual(model.notes.count, 1)
}
Add a persistence round trip — save notes to a temporary directory, load them back, assert equality. Then one failure test at your integration boundary: the Keychain item is missing, the child exits nonzero, the bookmark is stale. The failure path is the one you never click manually, which is exactly why it needs a test.
Finish with a manual pass that mimics a real user rather than a developer. Build the app, quit Xcode, and launch the .app from Finder — this catches environment assumptions, especially around PATH. Resize every window to its minimum. Drive the whole workflow with the keyboard, menus and shortcuts only. Open Settings, change something, relaunch, confirm it stuck. Find your data file in Finder and check it is where you documented.
The trap in a capstone is feature accumulation — adding tags, sync, and themes while the delete command still crashes on an empty selection. Resist it. A small utility whose every path works teaches you more, and demos better, than a large one that mostly works.
When it passes all of that, you have something real: a native macOS app with correct structure, durable data, and one honest system integration. That is the foundation the next course builds on — shipping it to other people’s Macs.
Lesson completed