Menus and Popovers in Menu Bar Apps for macOS

In this Menu Bar App tutorial you will learn how to present a menu and a popover that shows quotes from famous people. By Warren Burton.

Leave a rating/review
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Connect code to Interface Builder

You’ll notice that Xcode has placed circles in the left hand margin of your source editor. The circles are handles that appear when you use the @IBAction and @IBOutlet keywords.

You will now use them to connect your code to the UI.

While holding down alt click on Main.storyboard in the project navigator. This should open the storyboard in the Assistant Editor on the right and the source on the left.

Drag from the circle next to textLabel to the label in interface builder. In the same way connect the previous, next and quit actions to the left, right and bottom buttons respectively.

Note: If you have trouble with any of the above steps, refer to our library of macOS tutorials, where you’ll find introductory tutorials that will walk you through many aspects of macOS 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:

You used the default size of the view controller for the popover above. If you want a smaller or bigger popover, all you need to do is resize the view controller in the storyboard.

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.

Create actions for the buttons

If you haven’t already dismiss the Assistant Editor with Cmd-Return or View > Standard Editor > Show Standard Editor

Open QuotesViewController.swift and add the following properties to the class implementation:

let quotes = Quote.all

var currentQuoteIndex: Int = 0 {
  didSet {
    updateQuote()
  }
}

The quotes property holds all the quotes, and currentQuoteIndex holds the index of the current quote displayed. 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 viewDidLoad() {
  super.viewDidLoad()
  currentQuoteIndex = 0
}

func updateQuote() {
  textLabel.stringValue = String(describing: quotes[currentQuoteIndex])
}

When the view loads, 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, update the three action methods as follows;

@IBAction func previous(_ sender: NSButton) {
  currentQuoteIndex = (currentQuoteIndex - 1 + quotes.count) % quotes.count
}

@IBAction func next(_ sender: NSButton) {
  currentQuoteIndex = (currentQuoteIndex + 1) % quotes.count
}

@IBAction func quit(_ sender: NSButton) {
  NSApplication.shared.terminate(sender)
}

In next() and previous(), you cycle through the all the quotes and wrap around when you reach the ends of the array. quit terminates the app.

Build and run again, and now you can cycle back and forward through the quotes and quit the app!

final UI

Event Monitoring

There is one feature your users will want in your unobtrusive, small menu bar app, and that’s when you click anywhere outside the app, the popover automatically closes.

Menu bar apps should open the UI on click, and then disappear once the user moves onto the next thing. For that, you need an macOS global event monitor.

Next you’ll make an event monitor thats reusable in all your projects and then use it when showing the popover.

Bet you’re feeling smarter already!

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: Any?
  private let mask: NSEvent.EventTypeMask
  private let handler: (NSEvent?) -> Void

  public init(mask: NSEvent.EventTypeMask, handler: @escaping (NSEvent?) -> Void) {
    self.mask = mask
    self.handler = handler
  }

  deinit {
    stop()
  }

  public func start() {
    monitor = NSEvent.addGlobalMonitorForEvents(matching: 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.

Connect the Event Monitor

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: [.leftMouseDown, .rightMouseDown]) { [weak self] event in
  if let strongSelf = self, strongSelf.popover.isShown {
    strongSelf.closePopover(sender: event)
  }
}

This notifies your app of any left or right mouse down event and closes the popover when the system event occurs. Note that your handler will not be called for events that are sent to your own application. That’s why the popover doesn’t close when you click around inside of it. :]

You use a weak reference to self to avoid a potential retain cycle between AppDelegate and EventMonitor. It’s not essential in this particular situation because there’s only one setup cycle but is something to watch out for in your own code when you use block handlers between objects.

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!