On macOS the menu bar appears at the top of the screen at all times.
Whatever app is in the foreground controls what is displayed on the left of the menu bar. File, Edit, View, Go, Window and Help are examples of menus that are provided by Finder when it is in the foreground. The right side of the menu bar is not controlled by the currently active app, for instance the current date and time is displayed in the top right.
To the left of the clock is a row of icons, each of which was placed there by an app. Apple’s Human Interface Guidelines describe these icons as ‘menu bar extras’, and I’ll be creating one of these today.
Apple provides a warning about their visibility:
Avoid relying on the presence of menu bar extras. The system hides and shows menu bar extras regularly, and you can’t be sure which other menu bar extras people have chosen to display or predict the location of your menu bar extra.
Despite the fact my extra could be hidden when there are a lot of icons, I’m going to assume it’s visible most of the time.
Create a macOS app project in Xcode, remembering to choose SwiftUI as the interface type. I called mine MacMenuBar but you can call yours whatever you want.
The Xcode template has a ContentView
with a placeholder globe icon with a Text("Hello, world!")
underneath it.
You will not need to modify the ContentView
at all, because I’m going to be focused on the menu bar extra.
Since my project was called MacMenuBar, Xcode generated a type called MacMenuBarApp
. If yours was called YourAppName it would be called YourAppNameApp
. This is where I’m going to be writing my code, and the first step is adding the following line inside it:
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
This allows a class called AppDelegate to inherit from NSApplicationDelegate and provide the functionality of the menu bar extra.
When the app launches applicationDidFinishLaunching(_ aNotification: Notification)
is called by macOS, allowing me to complete the process of setting up the extra. The system will automatically remove the extra from the menu bar if it is referred to but not retained, so a reference needs to be stored a property of AppDelegate
. The app will crash if you try to initialise the NSStatusItem during launch, so that’s why I’m waiting until the app has launched to do anything.
I’m setting the icon to the same globe icon that is provided in ContentView
, but you can use any NSImage
you want. If you want to use an image from your Assets.xcassets you can use the NSImage(named:)
initialiser instead, passing in a string of the image name.
Finally the action for the button is set to a function appropriately named buttonAction()
.
The first task of the button is to bring the app to the foreground. This forces any app in the foreground to resign control of the left side of the menu bar, so you might notice that the menus there will change and your app’s name appears in the top left. Although an app is in the foreground, its windows may remain hidden.
I am looping through all windows associated with the app, although by default there are only two in this basic app. One of them is the window we expect that displays ContentView, and the other is actually the menu bar extra itself. In order to avoid modifying the visibility of the menu bar extra, I am filtering out any window that matches NSWindow.Level.statusBar
.
Now that I am only modifying windows that aren’t in the menu bar, I can toggle their visibility.
Toggling means that if it’s true it will become false, or if it’s false it will become true.
Now I have a menu bar extra that I can click whenever my app is in the background or minimised, and it will return to the foreground. If my app is in the foreground and
I don’t want to see it any more, clicking my menu bar extra will instantly hide it.