Applications placed in the menu bar have long been known to macOS users. Some of these applications have a “regular” part, others are placed only in the menu bar.
In this guide, you will write an application that shows several quotes from famous people in a pop-up window. In the process of creating this application, you will learn:
- assign an application icon in the menu bar
- make the application placed only in the menu bar
- add custom menu
- show a pop-up window at the user's request and hide it when necessary using Event Monitoring
Note: this guide assumes that you are familiar with Swift and macOS.
Getting started
Launch Xcode. Next in the
File / New / Project ... menu, select the
macOS / Application / Cocoa App template and click
Next .
On the next screen, enter Quotes as the
Product Name , select your
Organization Name and
Organization Identifier . Then make sure that Swift is selected as the application language and the
Use Storyboards checkbox is checked. Uncheck the checkboxes
Create Document-Based Application ,
Use Core Data ,
Include Unit tests and
Include UI Tests .
')

Finally, click
Next again, specify a place to save the project and click
Create .
Once the new project is created, open
AppDelegate.swift and add the following property to the class:
let statusItem = NSStatusBar.system.statusItem(withLength:NSStatusItem.squareLength)
Here we create in the menu bar the Status Item (application icon) of a fixed length that will be visible to users.
Then we need to assign our image to this new item in the menu bar so that we can distinguish our new application.
In the project navigator, go to Assets.xcassets,
upload a picture and drag it to the asset catalog.
Select a picture and open the attribute inspector. Change the
Render As option to
Template Image .

If you are using your own image, make sure that the image is black and white and configure it as a
Template image so that the icon looks great on both the dark and light menu bar.
Go back to
AppDelegate.swift , and add the following code to
applicationDidFinishLaunching (_ :) if let button = statusItem.button { button.image = NSImage(named:NSImage.Name("StatusBarButtonImage")) button.action = #selector(printQuote(_:)) }
Here we assign the image we just added to the application icon and assign the
action when we click on it.
Add the following method to the class:
@objc func printQuote(_ sender: Any?) { let quoteText = "Never put off until tomorrow what you can do the day after tomorrow." let quoteAuthor = "Mark Twain" print("\(quoteText) — \(quoteAuthor)") }
This method simply prints a quote to the console.
Pay attention to the
objc method
directive . This allows you to use this method as a response to a button click.
Build and run the application, and you will see the new application in the menu bar. Hooray!
Every time you click on the icon in the menu bar, the famous Mark Twain sentence is displayed in the Xcode console.
Hiding the main window and dock icon
There are a couple of little things that we need to do before tackling the functionality directly:
- remove dock icon
- delete unnecessary main application window
To remove the dock icon, open
Info.plist . Add a new key
Application is agent (UIElement) and set its value to
YES .

Now it's time to figure out the main application window.
- open the Main.storyboard
- select Window Controller scene and delete it
- Leave View Controller scene , we will use it soon

Build and run the application. Now the application does not have both the main window and the unnecessary icon in the dock. Fine!
Add to the Status Item menu
A single response to a click is clearly not enough for a serious application. The easiest way to add functionality is to add a menu. Add this function at the end of
AppDelegate .
func constructMenu() { let menu = NSMenu() menu.addItem(NSMenuItem(title: "Print Quote", action: #selector(AppDelegate.printQuote(_:)), keyEquivalent: "P")) menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem(title: "Quit Quotes", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")) statusItem.menu = menu }
And then add this call at the end of the
applicationDidFinishLaunching (_ :) constructMenu()
We create
NSMenu , add 3 instances of
NSMenuItem to it and set this menu as an application icon menu.
A few important notes:
- The title of the menu item is the text that appears in the menu. A good place to localize the application (if necessary).
- action , like action of a button or another control, is a method that is called when the user clicks on a menu item.
- keyEquivalent is a keyboard shortcut that you can use to select a menu item. Lower case characters use Cmd as a modifier, and upper case use Cmd + Shift . This only works if the application is at the top and active. In our case, it is necessary that the menu or some other window be visible, since our application does not have an icon in the dock.
- separatorItem is an inactive menu item as a gray line between other items. Use it to group
- printQuote is the method you have already defined in AppDelegate , and terminate is the method defined by NSApplication .
Launch the application, and you will see the menu by clicking on the application icon.

Try clicking on the menu — the
Print Quote option will print the quote to the Xcode console, and
Quit Quotes will complete the application.
Add a popup window
You've seen how easy it is to add a menu from code, but showing a quote in the Xcode console is clearly not what users expect from the application. Now we will add a simple view controller to show the quotes in the right way.
Go to the
File / New / File ... menu, select the
macOS / Source / Cocoa Class template and click
Next .

- name the class QuotesViewController
- make NSViewController a successor
- make sure the Also create checkbox XIB file for user interface is not checked
- set the language in Swift
Finally, click
Next again, select a place to save the file and click
Create .
Now open the
Main.storyboard . Expand
View Controller Scene and select
View Controller instance .

First select the
Identity Inspector and change the class to
QuotesViewController , then set the
Storyboard ID to
QuotesViewControllerNow add the following code to the end of the
QuotesViewController.swift file:
extension QuotesViewController {
What's going on here:
- get the link to Main.storyboard .
- create a Scene identifier that matches the one we just installed a little higher.
- Create an instance of QuotesViewController and return it.
You create this method, so now everyone who uses the
QuotesViewController does not need to know how it is created. It just works.
Notice the
fatalError inside the
guard statement. It can be a good idea to use it or
assertionFailure , so that if something in the development goes wrong, to yourself, and to other members of the development team, to be aware of.
Now back to
AppDelegate.swift . Add a new property.
let popover = NSPopover()
Then replace a
pplicationDidFinishLaunching (_ :) with the following code:
func applicationDidFinishLaunching(_ aNotification: Notification) { if let button = statusItem.button { button.image = NSImage(named:NSImage.Name("StatusBarButtonImage")) button.action = #selector(togglePopover(_:)) } popover.contentViewController = QuotesViewController.freshController() }
You changed the click action to the
togglePopover (_ :) method, which we will write later. Also, instead of setting up and adding a menu, we set up a popup window that will display something from the
QuotesViewController .
Add the following three methods to
AppDelegate :
@objc func togglePopover(_ sender: Any?) { if popover.isShown { closePopover(sender: sender) } else { showPopover(sender: sender) } } func showPopover(sender: Any?) { if let button = statusItem.button { popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY) } } func closePopover(sender: Any?) { popover.performClose(sender) }
showPopover () shows a popup window. You just indicate where it comes from, macOS positions it and draws an arrow as if it appears from the menu bar.
closePopover () simply closes the pop-up window, and
togglePopover () is a method that either shows or hides the pop-up window, depending on its state.
Launch the application and click on its icon.

Everything is great, but where is the content?
Implement Quote View Controller
First you need a model for storing quotes and attributes. Go to the
File / New / File ... menu and select the
macOS / Source / Swift File template , then
Next . Name the file
Quote and click
Create .
Open the
Quote.swift file and add the following code to it:
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") ] } extension Quote: CustomStringConvertible { var description: String { return "\"\(text)\" — \(author)" } }
Here we define a simple quotation structure and a static property that returns all quotes. Since we made
Quote conforming to the
CustomStringConvertible protocol, we can easily get conveniently formatted text.
There is progress, but we still need controls to display all this.
Adding interface elements
Open the Main.storyboard and pull out 3 buttons (
Push Button ) and a label (
Multiline Label) on the view controller.
Arrange the buttons and label so that they look like this:

Attach the left button to the left edge with a gap of 20 and center vertically.
Attach the right button to the right edge with a gap of 20 and center vertically.
Attach the bottom button to the bottom edge with a gap of 20 and center horizontally.
Attach the left and right edges of the label to the buttons with a gap of 20 center vertically.

You will see several layout errors, as there is not enough information for
auto layout to sort out.
Set the
Horizontal Content Hugging Priority label to 249 to allow the label to be resized.

Now do the following:
- set the image of the left button to the NSGoLeftTemplate and clear the title
- set the image of the right button to the NSGoRightTemplate and clear the title
- set the title buttons at the bottom in Quit Quotes .
- set the text alignment of the label in center.
- check that the line break of the label is set to Word Wrap .
Now open
QuotesViewController.swift and add the following code to the implementation of the
QuotesViewController class:
@IBOutlet var textLabel: NSTextField!
Add this extension to the class implementation. Now in
QuotesViewController.swift two class extensions.
We have just added an
outlet for the label that we will use to display quotes, and 3 stub methods that we connect to the buttons.
We connect code with Interface Builder
Pay attention: Xcode placed circles to the left of your code - near the keywords
IBAction and
IBOutlet .

We will use them to connect the code with the UI.
Hold down the
alt key and click on the
Main.storyboard in the
project navigator . This will open the
storyboard in the
Assistant Editor on the right, and the code on the left.
Drag the circle to the left of the
textLabel to the label in the
interface builder . In the same way, connect the
previous ,
next, and
quit methods with the left, right, and bottom buttons, respectively.

Run your application.

We used the default popup size. If you want a larger or smaller pop-up window, simply change its size in the
storyboard .
Write the code for the buttons
If you have not already hidden the
Assistant Editor , click
Cmd-Return or V
iew> Standard Editor> Show Standard EditorOpen
QuotesViewController.swift and add the following properties to the class implementation:
let quotes = Quote.all var currentQuoteIndex: Int = 0 { didSet { updateQuote() } }
The
quotes property contains all quotes, and
currentQuoteIndex is the index of the quotation that is currently being output.
CurrentQuoteIndex also has a
property observer to update the contents of the label with a new quote when the index changes.
Now add the following methods:
override func viewDidLoad() { super.viewDidLoad() currentQuoteIndex = 0 } func updateQuote() { textLabel.stringValue = String(describing: quotes[currentQuoteIndex]) }
When the view loads, we set the citation index to 0, which, in turn, causes the interface to be updated.
updateQuote () simply updates the text label to display a quote. corresponding
currentQuoteIndex .
Finally, update these methods with the following code:
@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) }
The methods
next () and
previous () cycle through all the quotes.
quit closes the application.
Run the application:

Event monitoring
There is one more thing that users expect from our application - hide the pop-up window when the user clicks somewhere outside of it. For this we need a mechanism called
macOS global event monitor .
Create a new Swift file, call it
EventMonitor , and replace its contents with the following code:
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 } } }
When initializing an instance of this class, we give it an event mask that we will listen to (such as keystrokes, mouse wheel scrolling, etc.) and an event handler.
When we are ready to start listening,
start () calls
addGlobalMonitorForEventsMatchingMask (_: handler :) , which returns an object that we save. As soon as the event contained in the mask happens, the system calls your handler.
To stop monitoring events,
removeMonitor () is called in
stop () and we remove an object by assigning it a nil value.
All we have to do is call
start () and
stop () at the right time. The class also calls
stop () in the de-initializer to tidy up after itself.
We connect Event Monitor
Open
AppDelegate.swift for the last time and add a new property:
var eventMonitor: EventMonitor?
Then add this code to configure the
event monitor at the end of the
applicationDidFinishLaunching (_ :) eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [weak self] event in if let strongSelf = self, strongSelf.popover.isShown { strongSelf.closePopover(sender: event) } }
This will inform your application when you click the left or right button. Note: the handler will not be called in response to mouse clicks inside your application. That is why the popup window will not close until you click inside it.
We use the
weak link to
self to avoid the danger of a cycle of strong links between
AppDelegate and
EventMonitor .
Add the following code to the end of the
showPopover (_ :) method:
eventMonitor?.start()
Here we start monitoring events when a popup window appears.
Now add the code at the end of the
closePopover (_ :) method:
eventMonitor?.stop()
Here we finish monitoring when the pop-up closes.
The app is ready!
Conclusion
Here you will find the complete code for this project.
You have learned how to set the menu and pop-up window in the application located in the menu bar. Why not experiment with a few tags or formatted text to get a better view of the quotes? Or connect backend to get quotes from the Internet? Or do you want to use the keyboard to navigate between quotes?
A good place for research is official documentation:
NSMenu ,
NSPopover and
NSStatusItem .