A hands-on guide to understanding Swift, SwiftUI, and macOS development by reading a real codebase — a menu bar app for Obsidian notes.
If you've never touched Swift or Xcode before, start here. We'll get you from zero to "I understand what's going on" in a few minutes.
1 Grab Xcode from the Mac App Store (it's free). This one download gives you everything — the Swift compiler, SwiftUI, the macOS SDK, a simulator, and a debugger.
2 Open Xcode once, go to Settings → Locations and make sure Command Line Tools is set. That's the only config you need.
1 Open Xcode → File → New → Project
2 Pick macOS → App. Choose "SwiftUI" for Interface and "Swift" for Language.
3 Give it a name, pick a folder. Xcode sets up
everything — a .xcodeproj, a starter SwiftUI view, the
whole structure.
Now hit ⌘R. Xcode compiles your code, signs the app, and launches it. You've got a running macOS app. The whole thing takes under a minute.
When your Mac launches an app, the OS needs someone to talk to. "Hey, you're
launching now." "Hey, the user just came back from sleep." That someone is
the AppDelegate — it's your app's point of contact with
macOS.
The big one is applicationDidFinishLaunching() — it fires
once at startup. Flowbar uses it to create the state, timer service, and
that menu bar icon you click. Think of it as your app's main().
Here's the thing — modern SwiftUI apps use
@main struct MyApp: App as their entry point. It's clean and
simple. But menu bar apps need AppKit stuff (the status bar icon, popovers,
global hotkeys) that SwiftUI just can't do yet.
So Flowbar uses both. The @main struct declares the app, and
@NSApplicationDelegateAdaptor plugs in an AppDelegate that
handles the AppKit side. You'll see this pattern in most macOS apps that
need low-level system access.
.xcodeproj — Xcode's project file. Build settings,
targets, which files belong where. You rarely edit this by hand.
.swift files — your actual code. No header files, no
makefiles, no boilerplate. Just Swift.
Info.plist — metadata like app name, version, and
permissions. Xcode handles most of it for you.
Assets.xcassets — images, app icons, colors. You drag
stuff in through Xcode's visual editor.
One nice thing: there's no main.swift when you
use @main. The attribute tells the compiler where to start.
Less ceremony.
Hover any node to see what it does and highlight its connections. Click to jump to the code.
AppDelegate, not pure SwiftUI — SwiftUI is great for UI, but it can't put an icon in your menu bar, show a popover from it, or listen for global hotkeys. For that you need AppKit, and AppKit talks through the AppDelegate.
SQLite, not Core Data or flat files — Timer sessions need real queries — "give me the total time per todo" is a GROUP BY + SUM. Core Data is way too heavy for one table. A JSON file can't do that. SQLite ships with every Mac, needs no setup, and the whole thing is about 10 lines of C API calls.
@Observable singletons —
AppState and TimerService use Swift's
@Observable
macro and are injected via
.environment(). Any view that needs them just asks with
@Environment. SwiftUI tracks property access at the call site — only views that actually
read a property re-render when it changes. No
@Published
needed.
GCD file watchers, not polling — Instead of checking the filesystem on a timer, Flowbar asks the OS to notify it the instant a file changes. Your CPU does nothing until something actually happens. That's DispatchSource.
Browse every file in the codebase with syntax highlighting and inline annotations. Click the numbered markers to learn Swift concepts.
← Choose a file from the sidebar
A cheat-sheet of Swift and SwiftUI concepts, each with a real example from the Flowbar codebase.
Follow the data through two key user interactions.
appState.selectFile(file).
activePanel = .file(file), then calls
loadFileContent(file).
.md file from disk via
String(contentsOf:).
@Observable macro detects the property change.
SwiftUI re-renders only views that read editorContent.
TextEditor bound to
editorContent shows the new note.
timerService.start(todoText:, sourceFile:).
DatabaseService.startSession().
Timer.scheduledTimer fires, updating
elapsed each tick.
@Observable macro tracks access — only views
reading timerService.elapsed re-render.
timerService.complete() ends the DB session, returns
the todo info.
MarkdownParser.markTodoDone() toggles
- [ ] to - [x] in the file.