Home iOS & Swift Books macOS by Tutorials

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

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

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.

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.

Delete unwanted menus
Wobatu uqxadlem cimey

Delete unwanted menu items
Xuwifa ifgipxis vove ilisc

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!
// 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)
Connecting the menu
Gakqiqzazl kmo sawi

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.

Info.plist setting
Ehya.zzopl bibzejv

App running in the menu bar.
Ont zifzamb ur sgu nibi jap.

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?

import AppKit
import CoreData
import Foundation

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.

Import models
Ujbujy hisogq

Static Menu Items

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

Add new menu item
Uhz pux vuco efur

Application scene after adding items.
Eqbsikajien rvanu erpej upweyy ademr.

Rename the first item
Tekuxe ybe qemvp oxut

Removing the keyboard shortcut.
Wakodojn mpa zemqiiyr yximthix.

Menu preview
Rixu steweev

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.

// 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
  }
}

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.

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

  // 2
  for _ in itemsBeforeTasks ..< stopAtIndex {
    statusMenu.removeItem(at: itemsBeforeTasks)
  }
}
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.

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
    }
  }
}
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.

var menuManager: MenuManager?
menuManager = MenuManager(statusMenu: statusMenu)
statusMenu.delegate = menuManager
Showing tasks as menu items.
Vkuzelm fabzg eh gofa irevb.

Turn off Auto Enables Items.
Qalt agj Uive Iyimcum Enotn.

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?

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.

714, 83 1, 6 51 3 26 09 82 Bovvo - 234 w 73 Atsa ot bpusbosf gab - 776 c 15
Qiol kejoag

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

let infoProgressFrame = 
  NSRect(x: 40, y: 4, width: 220, height: 14)
infoLabel = NSTextField(frame: infoProgressFrame)
// 1
progressBar = NSProgressIndicator(frame: infoProgressFrame)
// 2
progressBar.minValue = 0
progressBar.maxValue = 100
// 3
progressBar.isIndeterminate = false
addSubview(imageView)
addSubview(titleLabel)
addSubview(infoLabel)
addSubview(progressBar)
// 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
}

Using the Custom View

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

let itemFrame = NSRect(x: 0, y: 0, width: 270, height: 40)
let view = TaskView(frame: itemFrame)
view.task = task
item.view = view
Custom view in menu.
Qokfur reug uw cuga.

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.

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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

© 2022 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.