Building a Native macOS Menu Bar App in SwiftUI#
While building Funny Day Calendar — a macOS app about unusual, quirky holidays — I quickly realized that "just a window with a calendar" doesn't work as a product. The holiday of the day is something you want to glance at, not something you open an app to check. That means it belongs in the menu bar.
So the project ended up with three surfaces for the same content: a full window with the calendar and holiday details, a menu bar icon (MenuBarExtra) showing today's holiday, and a desktop widget. Here's what I learned building this in SwiftUI and shipping it to the Mac App Store (id 6773287898).
Why bother with a menu bar app at all#
The macOS menu bar isn't a "small window" — it's a different mode of existing as an app. Users don't launch you from the Dock or switch to you with Cmd+Tab; they notice the icon out of the corner of their eye. That changes what content belongs there: exactly as much information as fits in a single glance, and not a byte more.
For Funny Day Calendar that meant: the menu bar icon shows the holiday of the day in a single line, while everything else — holiday history, search, theme and background settings — stays in the main window. That split turned out to matter more than any implementation detail, but the implementation isn't trivial either, so let's start there.
MenuBarExtra: from icon to content#
Before SwiftUI, the only way to put an icon in the menu bar was AppKit's NSStatusItem — functional, but verbose, with manual NSPopover or NSMenu management. Starting with macOS 13, Apple added a declarative scene type, MenuBarExtra, that removes most of that boilerplate.
A basic implementation fits in a few lines:
import SwiftUI
@main
struct FunnyDayCalendarApp: App {
@StateObject private var holidayStore = HolidayStore()
var body: some Scene {
WindowGroup {
CalendarWindowView()
.environmentObject(holidayStore)
}
MenuBarExtra {
MenuBarContentView()
.environmentObject(holidayStore)
} label: {
MenuBarLabelView(holiday: holidayStore.todayHoliday)
}
.menuBarExtraStyle(.window)
}
}The key point: MenuBarExtra can be declared right next to a regular WindowGroup inside the same App. These are two independent scene types sharing one EnvironmentObject, so the holiday of the day, loaded once into HolidayStore, is available in sync in both the main window and the menu bar — no duplicated loading logic.
For the menu bar label, avoid long strings of text — macOS truncates a menu bar item when space runs out, and a combination of an SF Symbol with a short label reads much cleaner:
struct MenuBarLabelView: View {
let holiday: Holiday?
var body: some View {
if let holiday {
Label(holiday.shortTitle, systemImage: "calendar.badge.clock")
} else {
Image(systemName: "calendar")
}
}
}.window vs .menu: which one to pick#
MenuBarExtra has two content styles, and the difference between them is architectural, not cosmetic.
| Criterion | .menu (default) | .window |
|---|---|---|
| What it renders | A real NSMenu with menu items | Arbitrary SwiftUI content in a popup window |
| Interactivity | Only Button, Toggle, Divider, submenus | Any view: List, sliders, images, custom layout |
| Blocks the run loop | Yes, while the menu is open — animations and timers inside the app pause | No, the window behaves like a normal SwiftUI scene |
| Menu bar auto-hide | Works as expected | Known bugs around the menu bar reappearing while the popup is open over full-screen apps |
| Best for | Simple commands: "Refresh", "Settings", "Quit" | Rich previews: a calendar, a holiday card, themes |
For Funny Day Calendar the choice was obvious: the holiday of the day isn't a command, it's a mini card with an illustration and description, so .window fit better. The trade-off: you have to design your own click-outside-to-close behavior and watch out for menu bar auto-hide in full-screen mode — this is an open topic on the Apple Developer forums, and as of 2026 there's no clean system-level fix; you handle it manually through NSWindow delegates if the behavior matters for your UX.
Settings and the Dock icon: lifecycle quirks#
Once your app has a MenuBarExtra, the question comes up: should the app even show in the Dock? For Funny Day Calendar the answer is "usually yes," since this isn't a purely background utility — it has a real window. But users who prefer minimalism expect the option to hide the Dock icon and live in the menu bar only.
Technically this is solved with NSApplication.ActivationPolicy:
import AppKit
enum DockVisibility {
static func setHidden(_ hidden: Bool) {
NSApp.setActivationPolicy(hidden ? .accessory : .regular)
}
}.accessory removes the Dock icon and app switcher entry, the same way the LSUIElement key in Info.plist does, but it does it programmatically — so a "show in Dock" toggle can live in your app's UI rather than being locked in at build time.
The Settings scene is a separate pain point. SwiftUI offers a declarative API:
Settings {
SettingsView()
.environmentObject(holidayStore)
}and a system button, SettingsLink, to open it. In practice, SettingsLink invoked from a MenuBarExtra window while the app runs as .accessory doesn't always bring the settings window to the front — the menu bar stays active, and the settings window can open behind other apps. A working workaround is to explicitly activate the app before opening settings:
Button("Settings…") {
NSApp.activate(ignoringOtherApps: true)
NSApp.sendAction(
Selector(("showSettingsWindow:")),
to: nil,
from: nil
)
}Not the most elegant code I've written, but it's held up consistently across the macOS versions I tested Funny Day Calendar on.
A desktop widget: WidgetKit without a second app#
Since macOS Sonoma, WidgetKit widgets can live directly on the desktop, not just in Notification Center, and macOS Tahoe gave them a Liquid Glass background that adapts to your wallpaper. For Funny Day Calendar that meant reusing the same widget target as on iOS: the same WidgetKind, TimelineProvider, and SwiftUI layout for the holiday card — just with desktop availability added on top.
The real engineering challenge isn't rendering — it's data delivery. A widget extension is a separate process with no access to the main app's HolidayStore. Data crosses the boundary through an App Group:
struct HolidayProvider: TimelineProvider {
func getTimeline(
in context: Context,
completion: @escaping (Timeline<HolidayEntry>) -> Void
) {
let holiday = SharedHolidayStore.shared.todayHoliday()
let entry = HolidayEntry(date: .now, holiday: holiday)
// Refresh at the next midnight — the holiday of the day changes once a day
let midnight = Calendar.current.startOfDay(
for: .now.addingTimeInterval(86_400)
)
completion(Timeline(entries: [entry], policy: .after(midnight)))
}
func placeholder(in context: Context) -> HolidayEntry { .placeholder }
func getSnapshot(
in context: Context,
completion: @escaping (HolidayEntry) -> Void
) {
completion(.placeholder)
}
}When the main app updates data — say, after a theme or background change in settings — it has to explicitly ask the system to rebuild the timeline:
import WidgetKit
WidgetCenter.shared.reloadAllTimelines()Without that call, the widget keeps showing a stale snapshot until its next scheduled refresh — WidgetKit's refresh budget is intentionally limited to save battery, so you can't rely on "it'll update on its own."
App Sandbox and the road to the Mac App Store#
There's a common point of confusion worth clearing up: notarization (notarytool) is a process for apps distributed outside the Mac App Store, via a Developer ID. An app shipped through the Mac App Store doesn't go through notarization as a separate step — instead it's mandatorily App Sandboxed and goes through App Review.
App Sandbox turned out to be fairly forgiving for Funny Day Calendar: the app doesn't need network access (holiday data is local), so the entitlements needed were minimal:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.pro.dodecaidr.funnydaycalendar</string>
</array>
</dict>
</plist>The App Group entry is essential — it's the channel through which the main app and the widget extension exchange data inside the sandbox, where direct access to each other's files is off-limits.
One thing worth double-checking: both the main target and the widget extension must belong to the same App Group and the same Team ID — otherwise UserDefaults(suiteName:) silently returns nil, and the widget stays blank with nothing in the console to explain why.
App Review for a menu bar app: what actually mattered#
From an App Review standpoint, a MenuBarExtra app isn't some special category, but a couple of nuances showed up precisely because part of the functionality lives outside the main window.
- Minimum functionality (Guideline 4.2). If Funny Day Calendar consisted of nothing but a menu bar icon with no substantial main window, that would look like a textbook rejection candidate — "not enough functionality to justify a standalone app." A full window with a calendar, holiday history, and settings isn't just a UX decision — it's insurance against review pushback.
- Screenshots are still about the main window. Marketing screenshots for the Mac App Store listing need to show the core user experience at the required screen sizes — the menu bar icon on its own doesn't substitute for real interface shots.
- Review notes are never wasted. For functionality that doesn't surface in the main window (the menu bar icon, the desktop widget), it's worth spelling it out in the notes for the reviewer, so Apple's reviewer doesn't have to hunt for something that isn't obvious on first launch.
None of this is a workaround for the rules — it's more about making sure the reviewer sees exactly the same picture of the product a regular user does.
Wrapping up#
MenuBarExtra in SwiftUI removes most of the boilerplate that used to require manual AppKit work, but it doesn't remove the architectural decisions: which style to pick, how to keep data in sync across the main window, the menu bar, and the widget, how to handle the Dock icon and settings. For Funny Day Calendar, this combination — window, menu bar, widget — didn't end up as a checklist of features, but as three different ways of showing the same simple fact: what holiday is today.
If you want to see how it turned out, the app is called Funny Day Calendar, and it's available on the Mac App Store.



