Menu bar apps have been staple of OS X for along time. Many apps like 1Password and Day One have companion menu bar apps. Others like Fantastical live exclusively in OS X’s menu bar.
In this tutorial, you’ll build a menu bar app that shows inspirational quotes in a popover. While you do this, you’ll learn:
- How to create a menu bar icon
- How to make the app live exclusively in the menu bar
- How to add a menu for the user
- How to make a popover that shows on demand and hides when the user moves on — aka Event Monitoring
- How to add essential UI
Getting Started
Fire up Xcode. Go to File/New/Project… then select the OS X/Application/Cocoa Application template and click Next.
On the next screen, enter Quotes as the Product Name, choose your desired Organization Name and Organization Identifier. Then make sure that Swift is selected as the language, and uncheck Use Storyboards, Create Document-Based Application and Use Core Data.
Finally, click Next again, choose a place to save the project and click Create.
Once the new project is set up, open AppDelegate.swift and add the following property to the class:
let statusItem = NSStatusBar.systemStatusBar().statusItemWithLength(-2) |
This creates a Status Item — aka application icon — in the menu bar with a fixed length that the user will see and use.
Note: At the time of writing, there is a bug in Xcode that prevents it from recognizing NSSquareStatusItemLength
, but since it’s just a constant that’s defined as -2
, you just use -2
in it’s place.
Next, you’ll need to associate an image to the status item to make your app recognizable in the menu bar.
Go to Images.xcassets
. Then download this image StatusBarButtonImage@2x.png and drag it into the asset catalog.
Select the image and open the attributes inspector. Set Devices to Device Specific, and then make sure the Mac option is checked. Change the Render As option to Template Image.
If you use your own custom image, make sure that the image is black and white and configured as a template image so the Status Item looks great against both light and dark menu bars.
Back in AppDelegate.swift, add the following code to applicationDidFinishLaunching(_:)
:
if let button = statusItem.button { button.image = NSImage(named: "StatusBarButtonImage") button.action = Selector("printQuote:") } |
This will configure the status item with an icon of the image you just added, and an action for when you click on the item.
Before you get to test the app, you’ll need to add that button action method. Add the following method to the class:
func printQuote(sender: AnyObject) { let quoteText = "Never put off until tomorrow what you can do the day after tomorrow." let quoteAuthor = "Mark Twain" println("\(quoteText) — \(quoteAuthor)") } |
This method will simply log out a nice Mark Twain quote to the console.
Build and run the app, and you should see a new menu bar app available. You did it!
Note: If you have too many menu bar apps, you might not be able to see your button. Switch to an app with fewer menus than Xcode (like Finder) and you should be able to see it.
Every time you click on the menu bar icon, you’ll see the quote printed out in the Xcode console.
Hiding the Dock Icon and Main Window
There are still two small things to do before you have a functional menu bar app: hide the dock icon and kill off the main window.
To disable the dock icon, open Info.plist. Add a new key Application is agent (UIElement) and set its value to YES.
LSUIElement
.
Now it’s time to handle the main window. Open MainMenu.xib
and select the window object. Then, in the attributes inspector, set the window so it’s not visible at launch.
Build and run. You’ll see the app has no main window, no pesky dock icon and only a tidy status item in the menu bar!
Adding a Menu to the Status Item
Usually, a measly single action on click is not enough for a menu bar app. The easiest way to add more functionality to your app is to add a menu. Add the following code to the end of applicationDidFinishLaunching(_:)
:
let menu = NSMenu() menu.addItem(NSMenuItem(title: "Print Quote", action: Selector("printQuote:"), keyEquivalent: "P")) menu.addItem(NSMenuItem.separatorItem()) menu.addItem(NSMenuItem(title: "Quit Quotes", action: Selector("terminate:"), keyEquivalent: "q")) statusItem.menu = menu |
Here you create an NSMenu
, add multiple instances of NSMenuItem
to it, and then set the status item’s menu to that new menu.
A few things to note here:
- The title of a menu item is clear; it’s just the text that appears on the menu item.
- The action, like the action of a button or any control, is the method that gets called when you click the menu item.
- The
KeyEquivalent
is a keyboard shortcut that you can use to activate the menu item. A lowercase letter uses Cmd as the modifier key and an uppercase letter uses Cmd+Shift. This keyboard shortcut only works if the application is front-most and active. So, in this case, the menu or any other window needs to be visible, since the app has no dock icon. - A
separatorItem
is an inactive menu item that appears as a simple gray line between other menu items. Use it to group functionality in the menu. - The
printQuote:
action is the method you already defined inAppDelegate
. On the other hand,terminate:
is an action method defined in the shared application instance. Because you don’t implement it, the action gets sent up the responder chain until it arrives at the shared application, in which case the application quits.
Build and run, and you should see a menu when clicking on the status item. Progress!
Try out your options – selecting Print Quote will display the quote in the Xcode console, while Quit Quotes will quit the app.
Adding a Popover to the Status Item
You’ve seen how easy it is to set up a menu from code, but showing the quote in the Xcode console won’t cut it for your end users. The next step is to replace the menu with a simple view controller to show a quote right in place.
Go to File/New/File…, select the OS X/Source/Cocoa Class template and click Next.
Name the class QuotesViewController
, make it a subclass of NSViewController
, check the option Also create XIB file for user interface, and set the language to Swift.
Finally, click Next again, choose a place to save the file (In the Quotes subfolder of the project folder is a good place) and click Create.
Now, leave the new files alone and go back to AppDelegate.swift. Start by adding a new property declaration to the class:
let popover = NSPopover() |
Next, replace applicationDidFinishLaunching(_:)
with the following:
func applicationDidFinishLaunching(notification: NSNotification) { if let button = statusItem.button { button.image = NSImage(named: "StatusBarButtonImage") button.action = Selector("togglePopover:") } popover.contentViewController = QuotesViewController(nibName: "QuotesViewController", bundle: nil) } |
You’ve changed the button action to togglePopover:
which you’ll implement next. Also, rather than set up a menu, you’re setting up the popover to show whatever’s in QuotesViewController.
Finally, remove printQuote()
, and add the following three methods in its place:
func showPopover(sender: AnyObject?) { if let button = statusItem.button { popover.showRelativeToRect(button.bounds, ofView: button, preferredEdge: NSMinYEdge) } } func closePopover(sender: AnyObject?) { popover.performClose(sender) } func togglePopover(sender: AnyObject?) { if popover.shown { closePopover(sender) } else { showPopover(sender) } } |
showPopover()
displays the popover to the user. Similar to popovers on iOS, you just need to supply a source rect and OS X will position the popover and arrow so it looks like it’s coming out of the menu bar icon.
closePopover()
simply closes the popover, and togglePopover()
is the action method that will either open or close the popover depending on its current state.
Build and run, and then click on the menu bar icon to check that it shows and then hides an empty popover.
Your popover works great, but where’s all the inspiration? All you see is an empty view and no quotes. Guess what you’ll fix next?
Implementing the Quote View Controller
First, you’ll need model to store the quotes and attributions. Go to File/New/File… and select the OS X/Source/Swift File template, then click Next. Name the file Quote and click Create.
Open Quote.swift and add the following code to the file:
struct Quote { let text: String let author: String static let all: [Quote] = [ Quote(text: "Never put off until tomorrow what you can do the day after tomorrow.", author: "Mark Twain"), Quote(text: "Efficiency is doing better what is already being done.", author: "Peter Drucker"), Quote(text: "To infinity and beyond!", author: "Buzz Lightyear"), Quote(text: "May the Force be with you.", author: "Han Solo"), Quote(text: "Simplicity is the ultimate sophistication", author: "Leonardo da Vinci"), Quote(text: "It’s not just what it looks like and feels like. Design is how it works.", author: "Steve Jobs") ] } // MARK: - Printable extension Quote: Printable { var description: String { return "\"\(text)\" — \(author)" } } |
This defines a simple quote structure and a static property that returns all the quotes. Since you also make Quote
conform to Printable
, you can easily get a nice formatted string.
You’re making progress, but you still need some more function in the UI. How about some wrapping and auto constraints to make it pretty?
Open QuotesViewController.xib
and drag two bevel button instances, a label and a push button into the custom view.
Set the first bevel button’s image to NSGoLeftTemplate
and the second button’s image to NSGoRightTemplate
, set the label’s text alignment to Center and its line break mode to Word Wrap. Finally, set the title of the push button to Quit Quotes.
Here’s what the final layout should look like:
Can you add the auto layout constraints to make the user interface match? Give it a few good attempts before you open the spoiler below. If you get it right, skip the spoiler and give yourself a gold star.
After you’ve perfected the constraints, select Update Constraints by selecting Resolve Auto Layout Issues in the bottom-right corner of the canvas.
Now open QuotesViewController.swift
and replace the file contents with the following:
import Cocoa class QuotesViewController: NSViewController { @IBOutlet var textLabel: NSTextField! } // MARK: Actions extension QuotesViewController { @IBAction func goLeft(sender: NSButton) { } @IBAction func goRight(sender: NSButton) { } @IBAction func quit(sender: NSButton) { } } |
This starter implementation is for a standard NSViewController
instance. There’s one outlet for the text label, which you’ll need to update with the inspirational quote. The three actions are for the three buttons.
Then go back to QuotesViewController.xib and connect the outlet to the text label by control-dragging from File’s Owner to the label. Connect the actions by contro-dragging from each button to File’s Owner.
Note: If you have trouble with any of the above steps, refer to our library of OS X tutorials, where you’ll find introductory tutorials that will walk you through many aspects of OS X development, including adding views/constraints in interface builder and connecting outlets and actions.
Stand up, stretch and maybe do a quick victory lap around your desk because you just flew through a bunch of interface builder work.
Build and run, and your popover should look like this now:
The interface is finished, but you’re not done yet. Those buttons are waiting on you to know what to do when the user clicks them — don’t leave them hanging.
Open QuotesViewController.swift and add the following properties to the class:
let quotes = Quote.all var currentQuoteIndex: Int = 0 { didSet { updateQuote() } } |
The first property holds all the quotes, and the second holds the index of the current quote. currentQuoteIndex
also has a property observer to update the text label string with the new quote when the index changes.
Next, add the following methods to the class:
override func viewWillAppear() { super.viewWillAppear() currentQuoteIndex = 0 } func updateQuote() { textLabel.stringValue = toString(quotes[currentQuoteIndex]) } |
When the view appears, you set the current quote index to 0, which in turn updates the user interface. updateQuote()
simply updates the text label to show whichever quote is currently selected according to currentQuoteIndex
.
To tie it all together, implement the three action methods as follows;
@IBAction func goLeft(sender: NSButton) { currentQuoteIndex = (currentQuoteIndex - 1 + quotes.count) % quotes.count } @IBAction func goRight(sender: NSButton) { currentQuoteIndex = (currentQuoteIndex + 1) % quotes.count } @IBAction func quit(sender: NSButton) { NSApplication.sharedApplication().terminate(sender) } |
In goLeft()
and goRight()
, you cycle through the all the quotes and wrap around when you reach the ends of the array. quit()
terminates the app as explained before.
Build and run again, and now you can see all the quotes and quit the app!
Event Monitoring
There is one feature you’ll want in your unobtrusive, small menu bar app, and it’s when you click anywhere outside the app, the popover automatically closes.
Menu bar apps should open the popover on click or hover, and then disappear once the user moves onto the next thing. For that, you need an OS X global event monitor.
Here’s where you’ll take the concept to the next level. You’ll make the event monitor reusable in all your projects, and to keep the example app modular, you’ll define a Swift wrapper class and then use it when showing the popover.
Bet you’re feeling smarter already!

I feel S-M-R-T!
Create a new Swift File and name it EventMonitor, and then replace its contents with the following class definition:
import Cocoa public class EventMonitor { private var monitor: AnyObject? private let mask: NSEventMask private let handler: NSEvent? -> () public init(mask: NSEventMask, handler: NSEvent? -> ()) { self.mask = mask self.handler = handler } deinit { stop() } public func start() { monitor = NSEvent.addGlobalMonitorForEventsMatchingMask(mask, handler: handler) } public func stop() { if monitor != nil { NSEvent.removeMonitor(monitor!) monitor = nil } } } |
You initialize an instance of this class by passing in a mask of events to listen for – things like key down, scroll wheel moved, left mouse button click, etc – and an event handler.
When you’re ready to start listening, start()
calls addGlobalMonitorForEventsMatchingMask(_:handler:)
, which returns an object for you to hold on to. Any time the event specified in the mask occurs, the system calls your handler.
To remove the global event monitor, you call removeMonitor()
in stop()
and delete the returned object by setting it to nil
.
All that’s left is calling start()
and stop()
when needed. How easy is that? The class also calls stop()
for you in the deinitializer, to clean up after itself.
Open AppDelegate.swift
one last time, and add a new property declaration to the class:
var eventMonitor: EventMonitor? |
Next, add the code to configure the event monitor at the end of applicationDidFinishLaunching(_:)
:
eventMonitor = EventMonitor(mask: .LeftMouseDownMask | .RightMouseDownMask) { [unowned self] event in if self.popover.shown { self.closePopover(event) } } eventMonitor?.start() |
This notifies your app of any left or right mouse down event and closes the popover when the system event occurs.
Add the following code to the end of showPopover(_:)
:
eventMonitor?.start() |
This will start the event monitor when the popover appears.
Then, you’ll need to add the following code to the end of closePopover(_:)
:
eventMonitor?.stop() |
This will stop the event monitor when the popover closes.
All done! Build and run the app one more time. Click on the menu bar icon to show the popover, and then click anywhere else and the popover closes. Awesome!
Where To Go From Here?
Here is the downloadable final project with all of the code you’ve developed in the above tutorial.
You’ve seen how to set up both menus and popovers in your menu bar status items – why not keep experimenting with showing a randomized quote, connecting to a web backend to pull new quotes, or even some kind of “pro” version to open the door to monetization?
A good place to look for other possibilities is reading the official documentation for NSMenu
, NSPopover
and NSStatusItem
.
Pro tip: Be careful with the NSStatusItem
documentation. The API changes significantly in Yosemite, and sadly, the documentation marks all old methods as deprecated, but doesn’t document the new alternative methods. For that, you need to command click on NSStatusItem
in Xcode to look at the generated Swift header. There are only a few methods now and all the functionality is inside an NSButton
, so it’s pretty easy to comprehend.
Thanks for taking the time to learn how to make a cool popover menu app for OS X. For now, it’s pretty simple, but you can see that the concepts you’ve learned here are an excellent foundation for a variety of apps.
If you have any questions, awesome discoveries or ideas you’d like to bounce off others as you configure status items, menus or popovers in your apps, please let me know in the forum discussion below! :]
OS X Tutorial: Menus and Popovers in Menu Bar Apps is a post from: Ray Wenderlich
The post OS X Tutorial: Menus and Popovers in Menu Bar Apps appeared first on Ray Wenderlich.