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

8. Working with Timers, Alerts & Notifications
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.

In the previous chapter, you set up a menu bar app using AppKit. You designed a custom view to display the app’s tasks in the menu, and you monitored the menu so as to add and remove them as needed.

So far, the app isn’t doing anything active with the data, but that’s about to change!

Now, you’ll wire up menu items from the storyboard to your code and run a timer to track the progress of tasks and breaks. Then, you’ll look into using system alerts and local notifications to tell the user what’s going on.

Linking Outlets and Actions

You’ve created some static menu items to allow control of the app, but they don’t do anything yet. The first task is to link them to code, so you can access them and make them work.

In Xcode, open your project from the last chapter or open the starter project in the download materials for this chapter.

Open Main.storyboard and fully expand Application Scene in the Document Outline. Option-click AppDelegate.swift in the Project navigator to open it in a second editor.

Right now, you have a menu item titled Start Next Task but, if a task is running, it should have a different title. This means that you need to connect it to AppDelegate, so you can access the menu item programmatically. And since you want this menu item to do something, you also need to connect an AppDelegate method to it.

When you Control-drag from a storyboard into a .swift file, Xcode offers to create an Outlet or an Action. An outlet gives you a name you can use to refer to an object on the storyboard. An action works the other way around, giving the object on the storyboard a method it can call. Conveniently, if you Control-drag to near the top of your class, Xcode assumes you want to make an outlet, and if you Control-drag further down, it assumes an action.

You aren’t going to use all the connections yet, but since you’re here, it makes sense to set them all up.

Connecting the Outlets

Control-drag from Start Next Task to underneath where you declared menuManager. Don’t let go until you see the Insert Action or Outlet tooltip. You may have to move the mouse pointer down a line to get there:

Control-drag
Milgvuc-qlif

Connecting an outlet
Zetpiphuct ol aibcec

The outlet in code
Wdu uewmoj af fagi

Confirming the connection
Wupficrajp she yevfekheep

Wiring Up the Actions

Control-drag from Start Next Task into this blank space and connect an action called startStopTask:

Connecting an action
Zakgagpulx ig uzfuiz

The actions in code
Zve iqhuuxf iv dowe

Managing the Tasks

The standard Apple app architecture is MVC, which stands for Model View Controller. The model is the data, the view is how you display it and the controller sits in the middle. Unfortunately, it’s horribly easy to pile far too much responsibility onto the controller. That leads to a humorous new definition for the acronym: Massive View Controller.

class TaskManager {
  var tasks: [Task] = Task.sampleTasks
}
let taskManager = TaskManager()
for task in taskManager.tasks {

Timers

It’s finally time to talk about timers. After all, what’s the point of a timer app that can’t time anything? :]

import Combine
var timerCancellable: AnyCancellable?
func startTimer() {
  // 1
  timerCancellable = Timer
    .publish(
      // 2
      every: 1,
      // 3
      tolerance: 0.5,
      // 4
      on: .current,
      // 5
      in: .common)
    // 6
    .autoconnect()
    // 7
    .sink { time in
      print(time)
    }
}
init() {
  startTimer()
}
Timer publishing dates
Vunuw lirxisbovr lahik

Tracking the Timer State

When running this app, the timer can be in one of four states:

var timerState = TimerState.waiting

Starting and Stopping Tasks

Still in TaskManager.swift, add these methods for starting and stopping tasks:

// 1
func toggleTask() {
  // 2
  if let activeTaskIndex = timerState.activeTaskIndex {
    stopRunningTask(at: activeTaskIndex)
  } else {
    startNextTask()
  }
}

func startNextTask() {
  // 3
  let nextTaskIndex = tasks.firstIndex {
    $0.status == .notStarted
  }
  // 4
  if let nextTaskIndex = nextTaskIndex {
    tasks[nextTaskIndex].start()
    timerState = .runningTask(taskIndex: nextTaskIndex)
  }
}

func stopRunningTask(at taskIndex: Int) {
  // 5
  tasks[taskIndex].complete()
  timerState = .waiting
}
menuManager?.taskManager.toggleTask()
Task in progress
Jixp ax vzupfobx

Updating the Menu Title

There are three parts of the menu that you need to update: the menu title, the startStopMenuItem title and the tasks themselves.

func updateMenu(
  title: String,
  icon: String,
  taskIsRunning: Bool
) {
}
import AppKit
func checkTimings() {
  // 1
  let taskIsRunning = timerState.activeTaskIndex != nil

  // more checks here
  
  // 2
  if let appDelegate = NSApp.delegate as? AppDelegate {
    // 3
    let (title, icon) = menuTitleAndIcon
    // 4
    appDelegate.updateMenu(
      title: title,
      icon: icon,
      taskIsRunning: taskIsRunning)
  }
}
func updateMenu(
  title: String,
  icon: String,
  taskIsRunning: Bool
) {
  // 1
  statusItem?.button?.title = title
  statusItem?.button?.image = NSImage(
    systemSymbolName: icon,
    accessibilityDescription: title)

  // 2
  updateMenuItemTitles(taskIsRunning: taskIsRunning)
}

func updateMenuItemTitles(taskIsRunning: Bool) {
  // 3
  if taskIsRunning {
    startStopMenuItem.title = "Mark Task as Complete"
  } else {
    startStopMenuItem.title = "Start Next Task"
  }
}
.sink { _ in
  self.checkTimings()
}
Menu title counting down
Zeru xanzi cuovnosr keql

Updating the Tasks

You’ve done two of the three updates needed, but the menu item showing the active task is only updating when the menu opens or when you move the mouse pointer over it.

func updateMenuItems() {
  // 1
  for item in statusMenu.items {
    // 2
    if let view = item.view as? TaskView {
      // 3
      view.setNeedsDisplay(.infinite)
    }
  }
}
if menuManager?.menuIsOpen == true {
  menuManager?.updateMenuItems()
}
Menu items updating
Bulu ifebj esqosuqd

Checking the Timer

This is another job for TaskManager, so open TaskManager.swift and add these methods:

// 1
func checkForTaskFinish(activeTaskIndex: Int) {
  let activeTask = tasks[activeTaskIndex]
  if activeTask.progressPercent >= 100 {
    // tell user task has finished
    
    stopRunningTask(at: activeTaskIndex)
  }
}

// 2
func checkForBreakFinish(startTime: Date, duration: TimeInterval) {
  let elapsedTime = -startTime.timeIntervalSinceNow
  if elapsedTime >= duration {
    timerState = .waiting

    // tell user break has finished
  }
}

// 3
func startBreak(after index: Int) {
  let oneSecondFromNow = Date(timeIntervalSinceNow: 1)
  if (index + 1).isMultiple(of: 4) {
    timerState = .takingLongBreak(startTime: oneSecondFromNow)
  } else {
    timerState = .takingShortBreak(startTime: oneSecondFromNow)
  }
}
if taskIndex < tasks.count - 1 {
  startBreak(after: taskIndex)
}
switch timerState {
case .runningTask(let taskIndex):
  // 1
  checkForTaskFinish(activeTaskIndex: taskIndex)
case
  .takingShortBreak(let startTime),
  .takingLongBreak(let startTime):
  // 2
  if let breakDuration = timerState.breakDuration {
    checkForBreakFinish(
      startTime: startTime,
      duration: breakDuration)
  }
default:
  // 3
  break
}
On a short break
Oh i qrihv mjaek

On a long break
Ew o wosf khiuv

Creating Alerts

You’ll use NSAlert to communicate with your users. This is a standard dialog box where you supply a title, a message and, optionally, a list of button titles. In UIKit programming, you’d use UIAlertController for this.

// 1
let alert = NSAlert()
// 2
alert.messageText = title
alert.informativeText = message

// 3
for buttonTitle in buttonTitles {
  alert.addButton(withTitle: buttonTitle)
}

// 4
NSApp.activate(ignoringOtherApps: true)

// 5
let response = alert.runModal()
return response
// 1
let buttonTitles = ["Start Next Task", "OK"]

// 2
let response = openAlert(
  title: "Break Over",
  message: message,
  buttonTitles: buttonTitles)
  
// 3
return response

Showing Alerts

The alert code is all there now, but you still need to set up an instance of Alerter and add the calls to use it.

let interaction = Alerter()
// 1
if activeTaskIndex == tasks.count - 1 {
  // 2
  interaction.allTasksComplete()
} else {
  // 3
  interaction.taskComplete(
    title: activeTask.title,
    index: activeTaskIndex)
}
let response = interaction.breakOver()
if response == .alertFirstButtonReturn {
  startNextTask()
}
Task complete alert
Wakh xirpbome izekc

Break over alert
Xhuel ibec ewenk

Using Local Notifications

You may be wondering if local notifications would be a better choice for communicating with the user.

let interaction = Notifier()
func checkForBreakFinish(startTime: Date, duration: TimeInterval) {
  let elapsedTime = -startTime.timeIntervalSinceNow
  if elapsedTime >= duration {
    timerState = .waiting

    // Uncomment if using Alerter
    //  let response = interaction.breakOver()
    //  if response == .alertFirstButtonReturn {
    //    startNextTask()
    //  }

    // Uncomment if using Notifier
    interaction.startNextTaskFunc = startNextTask
    interaction.breakOver()
  }
}
Asking for notification permissions
Aqxahg tit yefawutiwoop reydujliujs

Task complete notification
Liqh tehbhoki zutativegeow

Break complete notification
Hniok qifvluma dutujiqicuek

Picking a User interaction

Which do you think is better: alerts or notifications?

Key Points

  • With AppKit apps, like UIKit apps, you have to make connections between the storyboard and the code.
  • There are two main ways to create a timer. This app uses a Combine TimerPublisher.
  • Using an enumeration is a great way of tracking changing states in your app.
  • Enumeration cases can have associated values, which make them even more powerful.
  • You can update the menu title and the menu items regularly, even if the menu is open.
  • With a custom NSView, you only have to tell it that its display needs updating to trigger a complete redraw.
  • System alerts provide a standard way of communicating with the user.
  • Local notifications are a way of contacting the user less intrusively.

Where to Go From Here?

At the start of this chapter, your app was displaying the task data in your status bar menu, but nothing else was happening.

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.
© 2024 Kodeco Inc.

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 Kodeco Personal Plan.

Unlock now