Chapters

Hide chapters

macOS by Tutorials

First Edition · macOS 12 · Swift 5.5 · Xcode 13

Section I: Your First App: On This Day

Section 1: 6 chapters
Show chapters Hide chapters

7. Using the Menu Bar for an App
Written by Sarah Reichelt

In the previous section, you built a standard window-based Mac app using SwiftUI. In this section, you’re going off in a completely different direction.

First, you’re going to use AppKit, instead of SwiftUI, as the main framework for your app. Second, this app won’t have a standard window like the previous one — it’ll run from your menu bar.

Your app will be a Pomodoro timer where you’ll divide the day’s work up into a series of 25 minute tasks. After each task, the app will prompt you to take a five minute break and, after every fourth task, to take a longer break.

Time-ato app
Time-ato app

Along the way, you’ll learn about menu bar apps, AppKit, timers, alerts, notifications and how to integrate SwiftUI views into an AppKit app.

Setting up the App

In Xcode, create a new project using the macOS App template. Set the name to Time-ato and the language to Swift. But this time, set the interface to Storyboard:

Project options
Project options

Save the app and take a look at the starting files:

Project files
Project files

This time, your starting .swift files are AppDelegate.swift and ViewController.swift. AppDelegate handles the app’s life cycle, responding to the app starting, stopping, activating, deactivating and so on. ViewController deals with the display of the initial window.

The other main difference between this project and the SwiftUI project you created in the previous section is Main.storyboard. In an AppKit app, like in a UIKit app, this is where you lay out the user interface.

Checking out the storyboard, there are three scenes:

Storyboard
Storyboard

  • Application Scene: The Main Menu and the App Delegate are the most important components.
  • Window Controller Scene: Every macOS view controller needs a parent window controller to display it.
  • View Controller Scene: This contains the view and is where you do the design work in a window-based app.

Converting the App into a Menu Bar App

Right now, this app is very similar to the app you created in the previous section except that it’s using AppKit. It has an initial window and a full menu bar. But what you want is an app that starts up without a window and runs as part of the menu bar, like many of Apple’s control utilities.

To convert your app project into a menu bar app, you need to do three things:

  1. Get rid of the window and most of the menus.
  2. Set up a status bar item linked to a menu.
  3. Configure a setting in Info.plist.

Getting Rid of the Window and Menus

First, get rid of the windows. In Main.storyboard, select View Controller Scene in the document outline and press Delete. Do the same with Window Controller Scene.

Expand Application Scene ▸ Application ▸ Main Menu. Select and delete all the menus except for Time-ato:

Delete unwanted menus
Delete unwanted menus

Next, expand Time-ato ▸ Menu and delete all the menus between the first Separator and the last Separator:

Delete unwanted menu items
Delete unwanted menu items

Finally, delete ViewController.swift from the Project navigator now that you’ve deleted its scene from the storyboard.

You’ve now removed all of the unwanted components.

Setting up the Status Bar Item

To create a status bar item, open AppDelegate.swift. Add these two properties to AppDelegate:

var statusItem: NSStatusItem?
@IBOutlet weak var statusMenu: NSMenu!

An NSStatusItem is an item that macOS displays in the system menu — that part of the menu bar to the right of your screen. It’ll hold the menu for your app. This naming is why you’ll often see apps like this referred to as status bar apps.

You’ll link the NSMenu to the menu in the storyboard. @IBOutlet marks this as a view that the interface builder can connect to.

Replace the contents of applicationDidFinishLaunching(_:) with:

// 1
statusItem = NSStatusBar.system.statusItem(
  withLength: NSStatusItem.variableLength)

// 2
statusItem?.menu = statusMenu

// 3
statusItem?.button?.title = "Time-ato"
statusItem?.button?.imagePosition = .imageLeading
statusItem?.button?.image = NSImage(
  systemSymbolName: "timer",
  accessibilityDescription: "Time-ato")

// 4
statusItem?.button?.font = NSFont.monospacedDigitSystemFont(
  ofSize: NSFont.systemFontSize,
  weight: .regular)

So what’s going on here?

  1. Add a new statusItem to the system-wide NSStatusBar and set it to have a variable length so that it can adjust to fit its contents.
  2. Set the NSMenu you declared earlier as the menu for the status item.
  3. Configure the status item with a title and a leading image. The actual display in the menu bar is a button. And using SF Symbols isn’t as easy when you’re not using SwiftUI!
  4. Set the font for the status item’s button. This will end up showing numbers as the timer counts down, but in the default system font, digits are proportional, so that 1 is not as wide as 9. This makes the display jump around as the numbers change. NSFont can give you a version of the system font with monospaced digits which will look much better in this case.

You’ve declared statusMenu and you’re using it in your statusItem, but now you need to go back to the storyboard and connect it.

Open Main.storyboard and Option-click AppDelegate.swift in the Project navigator to open it to the side. If Application Scene is not expanded, Option-click it to expand it fully. Control-drag from the Menu under Time-ato to statusMenu in AppDelegate.swift. Let go when you see a blue box around statusMenu and that will make the connection:

Connecting the menu
Connecting the menu

Close the secondary editor now to give yourself more room.

Configuring the Info.plist

Finally, you need to update Info.plist. Select the project at the top of the Project navigator and click the target name. Click the Info tab at the top, and if necessary, expand the Custom macOS Application Target Properties section.

Select any entry and click the Plus button in the center of its row to add a new row. Scroll until you can select Application is agent (UIElement) from the list, and change its value to YES (expand the pane if you can’t see the Value column on the right):

Info.plist setting
Info.plist setting

You’re finally ready to build and run your menu bar app. No window appears and no icon bounces in the Dock. Look at the right side of your menu bar. There is your menu bar app and clicking the app name pops down your menu with two working menu items:

App running in the menu bar.
App running in the menu bar.

Note: If you try the About Time-ato menu item, it may look like nothing happened. Hide your open apps and you’ll see the About box. It’s visible but not brought to the front. You’ll fix that later.

Why AppKit?

Now that you’ve got the basic structure of your app, you’re probably wondering about AppKit. What is AppKit and why is this app using it?

Why is the easy question to answer. SwiftUI is still very new, but AppKit has been around for more than 20 years. This means that the majority of macOS apps use AppKit. If you’re looking for a job developing Mac apps, or you want to work on some open-source Mac project, chances are that it’ll be using AppKit. So while SwiftUI is the future, AppKit is still dominant and it’s important to learn something about how it works.

Now on to what is AppKit? Explaining this needs a history lesson.

In 1985, Steve Jobs was forced out of Apple. He founded NeXT (yes, they really did write it that way), a computer company specializing in high-end computers for business and research. NeXT developed the NeXTSTEP platform, which was an operating system based on Unix. When Jobs returned to Apple in 1997, he brought NeXTSTEP with him, and it became the basis for OS X and later macOS.

The application environment for programming Macs was named Cocoa and, if you look at AppDelegate.swift, you’ll see the only library imported in the file is Cocoa.

Command-click the word Cocoa and select Jump to Definition. This reveals a file containing three other imports:

import AppKit
import CoreData
import Foundation

Cocoa is a super-group containing Foundation, which is the basic library underneath all Apple device programming, CoreData for databases and AppKit for the user interface.

Command-click AppKit and again, select Jump to Definition. This time you’ll see an enormous list of all the NS objects that make up AppKit. And now that you know this all came from NeXTSTEP, you can see why they all have the prefix NS.

Interestingly, the name Cocoa was originally trademarked by Apple for a multimedia project. When they needed a name for their new application development system, they decided to reuse Cocoa to avoid the delay of registering a new trademark.

CocoaTouch came later for programming iOS devices, and in place of AppKit, it used UIKit. Since Apple engineers created UIKit from scratch, they were able to choose the more logical prefix of UI. Many of the interface elements are the same between the two frameworks, just swapping NS and UI. Others appear to be the same but have different properties and methods, which is something you need to check if you’re converting any projects.

Adding the Models

Now that you know a little bit about the history of AppKit, it’s time to get back to the app. Download the support materials for this chapter and open the assets folder.

Drag the Models folder from assets into your Project navigator, selecting Copy items if needed, Create groups, and checking the Time-ato target:

Import models
Import models

Looking in the Models group, Task is the structure that defines the tasks the app will time. It also has an extension with static properties that provide sets of sample tasks. TaskTimes sets the length of each phase in the process, using a set of shorter times in debug mode to make the app faster to test. TaskStatus is an enumeration that defines the possible states of a task. Each state has its own text, color and icon that you’ll eventually display in the menu.

Static Menu Items

The menu will contain two types of items: static items to control the app and dynamic items to display the tasks.

Start with the static items. Open Main.storyboard and fully expand the Application Scene.

Click the + in the top right of the window or press Shift-Command-L to open the Library. Search for “menu” and drag a Menu Item to between the two Separators in the document outline:

Add new menu item
Add new menu item

Hold down Option and drag the new Item down one row to duplicate it. Repeat this so that you end up with three new entries labeled Item.

Use the same method to duplicate the first Separator. Your Application Scene will now look like this:

Application scene after adding items.
Application scene after adding items.

Select the first new Item, show the Inspectors on the right, select the Attributes inspector and change the title to Start Next Task:

Rename the first item
Rename the first item

In the same way, change the titles of the other items to:

  • Edit Tasks… (use Option-; to type the ellipsis symbol.)
  • Launch on Login

Finally, select the Quit Time-ato menu item and click the X to remove the Key Equivalent:

Removing the keyboard shortcut.
Removing the keyboard shortcut.

Since menu bar apps have no window to accept focus, they can’t respond to keyboard shortcuts.

You’re now finished with the storyboard and the preview of your menu in the storyboard looks like this:

Menu preview
Menu preview

You’ll place the dynamic items programmatically, between the top two separators.

Dynamic Menu Items

To add, remove and update the menu items representing the tasks, you’ll add a MenuManager class. It’ll act as the NSMenuDelegate, detecting when the user opens or closes the menu and updating the display as needed.

Create a new Swift file called MenuManager.swift. Replace its contents with:

// 1
import AppKit

// 2
class MenuManager: NSObject, NSMenuDelegate {
  // 3
  let statusMenu: NSMenu
  var menuIsOpen = false

  // 4
  var tasks = Task.sampleTasksWithStatus

  // 5
  let itemsBeforeTasks = 2
  let itemsAfterTasks = 6

  // 6
  init(statusMenu: NSMenu) {
    self.statusMenu = statusMenu
    super.init()
  }

  // 7
  func menuWillOpen(_ menu: NSMenu) {
    menuIsOpen = true
  }

  func menuDidClose(_ menu: NSMenu) {
    menuIsOpen = false
  }
}

So what is this doing?

  1. Import AppKit, which you need for working with NSMenu.
  2. Declare the class conforms to NSMenuDelegate. This requires that the class also conforms to NSObject.
  3. Set up properties to hold the menu and a flag that will reflect whether the menu is open or not.
  4. Import a set of sample tasks for use during development.
  5. Tell the class how many menu items come before where the tasks should go, and how many after.
  6. Initialize the class with its menu.
  7. Add the menu delegate methods, which will detect the menu opening and closing and set the menuIsOpen flag.

Clearing the Menu

When the menu closes, you must remove any existing task menu items. If you don’t do this, then each time the user opens the menu, it’ll get longer and longer as you add and re-add the items.

Add this method to MenuManager:

func clearTasksFromMenu() {
  // 1
  let stopAtIndex = statusMenu.items.count - itemsAfterTasks

  // 2
  for _ in itemsBeforeTasks ..< stopAtIndex {
    statusMenu.removeItem(at: itemsBeforeTasks)
  }
}

And going through these steps:

  1. You’ve already defined two properties dictating where the tasks start and end in the menu. Use the property which defines the end to work out how many items to delete. You use statusMenu.items to get a list of the existing items in the menu.
  2. Loop through the items removing the one at index 2 every time until you’ve removed the right number.

Now, you can add a call to this method inside menuDidClose(_ :):

clearTasksFromMenu()

Adding to the Menu

That’s done the tidying up part, so the next thing is to create new menu items to display the tasks.

Start by adding this new method to MenuManager:

func showTasksInMenu() {
  // 1
  var index = itemsBeforeTasks
  var taskCounter = 0

  // 2
  for task in tasks {
    // 3
    let item = NSMenuItem()
    item.title = task.title

    // 4
    statusMenu.insertItem(item, at: index)
    index += 1
    taskCounter += 1

    // 5
    if taskCounter.isMultiple(of: 4) {
      statusMenu.insertItem(NSMenuItem.separator(), at: index)
      index += 1
    }
  }
}

There’s quite a lot happening here:

  1. Use the predefined property as the starting position for the first task entry and add a counter to keep track of how many tasks you add to the menu.
  2. Loop through all the tasks.
  3. Create an NSMenuItem for each task, setting its title to the title of the task.
  4. Insert this menu item into the menu and increment the index for positioning the next one. Increment the task counter too.
  5. The Pomodoro technique suggests that you take a longer break after every fourth task. Use taskCounter to check if this is a fourth task and, if so, add a separator line after it.

And again, you need to call this method, so add this line to menuWillOpen(_ :):

showTasksInMenu()

Joining it All Up

You’ve done some good work, setting up MenuManager and providing methods to clean and populate the menu. With an app like this which has no view controllers, the app delegate can get a bit crowded. Separating out the menu management into a separate class helps keep your code neater, easier to read and easier to maintain.

The next step is to connect MenuManager back to AppDelegate.

Over in AppDelegate.swift, add this definition:

var menuManager: MenuManager?

Next, add these two lines to the end of applicationDidFinishLaunching(_:):

menuManager = MenuManager(statusMenu: statusMenu)
statusMenu.delegate = menuManager

Now, you’ve got an instance of MenuManager, initialized with the statusMenu and set up as the delegate for that menu.

And now, it’s time to build and run:

Showing tasks as menu items.
Showing tasks as menu items.

There are all the sample tasks with a separator after every fourth one, wrapped in your static menu items. And, if you close the menu and re-open it, you still only see one copy of the tasks list.

You may be wondering why the menu items are all grayed out. There is a menu setting to auto-enable menu items and because you don’t have actions attached to these menu items, the menu has disabled them for you.

To fix this, open Main.storyboard and select the menu under Time-ato. In the Attributes inspector uncheck Auto Enables Items:

Turn off Auto Enables Items.
Turn off Auto Enables Items.

Styling the Menu Items

At the moment, your tasks appear in the menu but only as text, which leaves out a lot of information. Which tasks are complete? If a task is in progress, how long has it been running?

While most menu items are text, you can specify any NSView, or subclass of NSView, for a menu item. So, you’ll create a custom view and use it to display the tasks in the menu.

When creating a custom view using AppKit or UIKit, you can use storyboards — or .xib files — to create them visually, or you can create them programmatically. The debate over which is best can get quite heated, almost rivaling the great debate over tabs versus spaces! :]

Storyboards allow you to see your design. Xcode provides helpful markers to make sure you follow the Human Interface Guidelines for spacing and positioning. It’s easy to see what settings you can apply to each subview.

Writing your view in code can get very verbose, but it gives you very precise control and makes version control clearer and easier to follow.

In this case, you could create a new view controller with an .xib file. This would allow you to design your layout visually. Then, for each menu item, you would create an instance of your view controller and use its view in the menu item. You can’t easily create an NSView in the storyboard without a view controller.

This will work and make designing the view easier, but when you come to making frequent updates to the views, it’ll be more clumsy. So while visual design is usually more productive, in this case, you’re going to create the view in code, to improve performance.

Creating a Custom View

Open the assets folder in the downloaded materials for this chapter. Drag TaskView.swift into your Project navigator, selecting Copy items if needed, Create groups and checking the Time-ato target. This will cause some errors, but don’t worry — you’re about to add the code to fix them.

Looking at the file, notice that it is a class that inherits from NSView. This means that you can use it anywhere you could use an NSView — like in a menu item.

The class has an optional Task property and four other properties that are all NSView subclasses.

There are four components that you’ll show in this custom view:

  • The task’s title.
  • An icon showing its status.
  • A progress bar if it’s running.
  • Some information text if it’s not running.

You’re going to lay out these components as shown in this diagram:

270, 40 0, 0 10 4 20 10 40 Title - 220 x 16 Info or progress bar - 220 x 14
View layout

There are three important things to note:

  1. The overall size of the view is 270 x 40. This will suit the expected content.
  2. The origin point — (0, 0) — is at the bottom left. If you’ve used UIKit, you’re probably expecting the origin to be at the top left.
  3. If the progress bar is visible, the info text will be blank, so they are both in the same position.

Start by setting up the image. In TaskView.swift, under // set up subviews, add this:

// 1
let imageFrame = NSRect(x: 10, y: 10, width: 20, height: 20)
// 2
imageView = NSImageView(frame: imageFrame)
// 3
imageView.imageScaling = .scaleProportionallyUpOrDown

What are these lines doing?

  1. Create an NSRect with an origin and a size. The diagram shows the origin, and the width and height are half the total height. If you were making a UIView, you’d use a CGRect here.
  2. InItialize imageView, using the NSRect as its frame.
  3. Change one of the default settings so that the image will scale either up or down to fit the frame.

Next, for the two text views, insert these lines under the ones you just added:

let titleFrame = NSRect(x: 40, y: 20, width: 220, height: 16)

let infoProgressFrame = 
  NSRect(x: 40, y: 4, width: 220, height: 14)

These are very similar and set up two more NSRects to use when initializing the two NSTextField views. Check the diagram to see how the frames line up.

The blocks of code supplied below configure these NSTextField views. They’re non-editable, without bezels and with appropriate font sizes. The infoLabel will never have enough text to overflow, but the titleLabel might, so you set it to truncate the tail of the text if needed.

The last view is the progress bar, so add this now:

// 1
progressBar = NSProgressIndicator(frame: infoProgressFrame)
// 2
progressBar.minValue = 0
progressBar.maxValue = 100
// 3
progressBar.isIndeterminate = false

This is a bit different:

  1. Use the same frame you used for infoLabel to initialize an NSProgressIndicator.
  2. Set the minimum and maximum values for showing progress as a percentage.
  3. An indeterminate progress indicator is something like a spinner that shows the app is busy but doesn’t provide any information about the state of progress. This progress bar will NOT be indeterminate, so that it can show what percentage of the task is complete.

You’ve now initialized and configured the four subviews, but there is one last step. In order to show them, you must add them to the custom view.

Replace // add subviews with:

addSubview(imageView)
addSubview(titleLabel)
addSubview(infoLabel)
addSubview(progressBar)

Now, when you initialize a TaskView, this will size, configure and place its four subviews.

The errors have all gone away, but you’ve still got a warning. Whenever the view needs updating, it calls its draw(_:) method, so here is where you can actually display the task data.

Replace // view update code with this, which will silence the warning:

// 1
let color = task.status.textColor

// 2
imageView.image = NSImage(
  systemSymbolName: task.status.iconName,
  accessibilityDescription: task.status.statusText)
imageView.contentTintColor = color

// 3
titleLabel.stringValue = task.title
titleLabel.textColor = color

// 4
infoLabel.stringValue = task.status.statusText
infoLabel.textColor = color

// 5
switch task.status {
case .notStarted:
  progressBar.isHidden = true
case .inProgress:
  progressBar.doubleValue = task.progressPercent
  progressBar.isHidden = false
case .complete:
  progressBar.isHidden = true
}

This code will run every time the view changes, if the view has a Task, so what is it doing?

  1. Get a color from the task’s status.
  2. Set up the image with an SF Symbol from the task’s status and tint it with the color.
  3. Show the task’s title with the selected color.
  4. Get the task’s status text and display it with the correct color. If the task is in progress, this is an empty string, so there’s no need to hide the view.
  5. Use the task’s status to determine whether the progress bar should be visible and to set its value, if the task is in progress.

So now you’ve defined a custom NSView, connected it to a Task and it’s all ready for use in your menu.

Using the Custom View

To apply this view in the menu, go back to MenuManager.swift and find showTasksInMenu().

Below where you created the taskCounter property, add this to specify the fixed size for the menu item views:

let itemFrame = NSRect(x: 0, y: 0, width: 270, height: 40)

Next, replace item.title = task.title with:

let view = TaskView(frame: itemFrame)
view.task = task
item.view = view

This creates a TaskView with its frame, assigns it a task and sets it as the menu item’s view.

You’ve done a lot of work in this section, but at last, it’s time for another build and run:

Custom view in menu.
Custom view in menu.

And now, you can see what your custom view looks like. The view has grayed out the completed tasks and marked them as Complete. The task in progress has a progress bar and uses the system accent color. The remaining tasks have Not started yet as their information text. And each of the three types of tasks has an appropriate icon.

Great work! There was a lot to get through in this chapter, but now you have a menu bar app that displays your model in a custom view.

Challenge

Challenge: Change the Colors

Open TaskStatus.swift and look at the textColor computed property. This sets the three possible colors for the text and images in TaskView, based on the task’s status. The colors used are predefined by NSColor.

Open Xcode’s documentation and search for UI Element Colors. Scroll through the list of possibilities.

Right now, TaskView uses labelColor, controlAccentColor and placeholderTextColor. What other options could you use in these places? Try out a few alternatives. Don’t forget to check the menu in light and dark modes and with different system accent colors.

Key Points

  • A menu bar or status bar app seems very different as it runs without a main window or Dock icon, but underneath, it’s quite similar.
  • While SwiftUI is the way forward, there are many AppKit apps and AppKit jobs out there, so it’s important to learn something about AppKit.
  • Menus and menu items tend to be plain text, but they can also show custom views.
  • Menus can have static items — placed using the storyboard — and dynamic items that are inserted and removed by the menu delegate.
  • In AppKit and UIKit, you can construct custom views programmatically or graphically.

Where to Go From Here?

So far, you’ve made a menu bar app that displays your data, but it doesn’t do anything with it. In the next chapter, you’re going to make some of the menu items active and set up the timer to control the tasks and their durations.

You’ll also learn how to communicate with the user via alerts and notifications.

Check out this link if you would like to learn more about the Pomodoro technique.

© 2026 Kodeco Inc.