macOS integration
Build a menu bar extra
Expose a small set of frequent actions from the system menu bar without turning the extra into a second complete application.
12 minute lesson
The icons at the right end of the menu bar are menu bar extras. They exist for one job: giving the user something quick without making them find your window. For our notes app, that means capturing a thought in two clicks from inside any other app.
SwiftUI turned what used to be a pile of NSStatusItem code into a scene. Declare it beside the others:
MenuBarExtra("Notes", systemImage: "note.text") {
MenuBarContent()
.environmentObject(model)
}
The content is a normal view, so environment actions work inside it:
struct MenuBarContent: View {
@EnvironmentObject private var model: NotesModel
@Environment(\.openWindow) private var openWindow
var body: some View {
Button("New Note") { model.createNote() }
Divider()
Button("Open Notes") { openWindow(id: "main") }
}
}
For that last button to work, give the main scene an identifier: WindowGroup(id: "main") { … }.
By default the content renders as a menu — a plain list of commands, like the Wi-Fi icon. There is a second style that shows a full view in a floating panel:
MenuBarExtra("Notes", systemImage: "note.text") {
QuickNoteView()
.frame(width: 300, height: 200)
}
.menuBarExtraStyle(.window)
Choose the menu style unless the content genuinely needs controls a menu cannot hold, like a text field for typing a quick note. The window style is heavier and behaves differently — it stays open while the user interacts, and you become responsible for making it feel dismissable.
The string label matters even though users see the icon. VoiceOver reads it, and it identifies the item when users rearrange extras by ⌘-dragging them.
Run the app and look at the top right of the screen. The note icon should sit among the system extras. Click it, create a note from the menu, then open the main window and confirm the note is there — both scenes share one model, which is why we inject the same instance everywhere.
The mistake with menu bar extras is scope creep. It starts as three commands, then grows tabs, settings, and scrolling lists, until you have built a second app inside a popover. When the extra needs that much, the answer is opening the real window. Keep the extra as the shortcut, not the destination.
One more warning: if the extra becomes the app’s only interface, with the Dock icon hidden, add an explicit Quit button to its menu. Without a Dock icon, that menu is the only place the user can quit from.
Lesson completed