What I Learned Building a Native macOS Menu Bar App
Building VPN Peek taught me more about macOS development than any tutorial could. Here’s what I learned shipping a native menu bar utility.
NSPopover vs NSMenu: Choose Wisely
I started with NSPopover because it looked modern and flexible. Big mistake.
The popover has a slight delay, doesn’t dismiss naturally, and looks like a “floating app” rather than a system utility. Users expect menu bar apps to behave like system menus—instant response, click-away dismissal.
I switched to NSMenu with custom SwiftUI views:
let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
let menu = NSMenu()
let customItem = NSMenuItem()
customItem.view = NSHostingView(rootView: YourSwiftUIView())
menu.addItem(customItem)
statusItem.menu = menu
This approach gives you instant responsiveness and native macOS behavior. The menu appears immediately when clicked, dismisses when you click away, and feels like a system component.
Lesson: Start with NSMenu from day one. Don’t fight platform conventions.
The SwiftUI-AppKit Hybrid
Pure SwiftUI for macOS is tempting but frustrating. The framework is designed for iOS, and macOS features feel like afterthoughts.
I settled on a pragmatic 70/30 split:
- SwiftUI for views and state management
- AppKit for system integration (menu bar, window management, permissions)
This hybrid approach works. SwiftUI excels at reactive UI and state. AppKit handles the macOS-specific details that SwiftUI doesn’t support well.
Lesson: Don’t fight platform constraints. Use the right tool for each job.
Sandbox Limitations Force Better Focus
macOS App Store sandboxing prevented several features I wanted:
- Accessing other applications’ network connections
- Reading system logs
- Monitoring processes
Initially frustrating, but the constraint improved the app. Instead of trying to do everything, VPN Peek does fewer things well. The sandbox forced clarity.
Lesson: Constraints breed focus. The app is better for what it can’t do.
Polish Details Users Notice
Adaptive Menu Bar Icons
Template images automatically adjust for light/dark themes:
statusItem.button?.image?.isTemplate = true
This single line makes your icon look native. Users won’t consciously notice, but they’ll feel the app belongs on their system.
No Dock Icon
Menu bar apps shouldn’t appear in the Dock. Add this to Info.plist:
<key>LSUIElement</key>
<true/>
This removes the Dock icon and keeps the app where it belongs—in the menu bar only.
Number Formatting Matters
Don’t display “1024KB” when you can show “1.0 MB”. Use NumberFormatter for proper localization and formatting. These details communicate care.
Lesson: Native apps feel native because of hundreds of small details.
App Store Review: Perception Over Logic
First submission: rejected for “insufficient functionality.”
I added:
- Keyboard shortcuts
- Preferences window
- Menu bar customization options
Resubmitted. Approved.
Here’s the thing: no core logic changed. The app did the same thing. But it looked more complete. Reviewers assess completeness in seconds, not hours.
Lesson: Perception matters. A preferences window and keyboard shortcuts signal “finished app” even if they’re not essential features.
Technical Debt Accumulates Fast
I delayed building a proper preferences window. “Later” became “now” when I needed to add settings for new features. Refactoring the settings system took longer than building it correctly from the start.
Lesson: Build foundational UI (preferences, settings) early. You’ll need it sooner than you think.
Target Modern macOS Versions
I initially targeted macOS 12 for broader compatibility. This meant:
- No
@Observablemacro - Workarounds for SwiftUI limitations
- More complex state management
Raising the minimum to macOS 14 cut hundreds of lines of code and made development faster.
Lesson: Unless you need legacy support, target recent macOS versions. The API improvements are worth the narrower audience.
Building Native Feels Different
Web development is forgiving. Platform development is opinionated.
macOS has expectations:
- Menu bar icons should be template images
- Preferences should be in a standard window
- Keyboard shortcuts should follow conventions
Fighting these patterns is painful. Following them makes your app feel like it belongs.
Lesson: When the platform has opinions, listen. Native development rewards conformity.
What I’d Build Differently
Starting over, I’d:
- Use NSMenu from day one - Skip the NSPopover experiment
- Build preferences window first - Avoid technical debt
- Target macOS 14+ - Access modern Swift features immediately
- Accept sandbox limitations - Design around them, don’t fight them
The Humbling Part
Building for macOS is humbling. The platform has been refined for decades. Users have high expectations. Every rough edge is visible.
But when you get it right—when the app feels native, responds instantly, and solves a real problem—the satisfaction is unlike web development. You’ve created something that feels permanent, not ephemeral.
Moving Forward
VPN Peek is on the Mac App Store. I’m already applying these lessons to the next app.
The takeaway: respect the platform, embrace constraints, and ship something that feels like it belongs. Native development is harder than web development, but the result is worth it.
Now, back to building.