macOS Development for Beginners: Part 3

In this macOS Development tutorial for beginners, learn how to add the code to the UI you developed in part 2 to make a fully operational egg timer. By Roberto Machorro.

4.3 (22) · 1 Review

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

Implementing Selected Preferences

The Preferences window is looking good - saving and restoring your selected time as expected. But when you go back to the main window, you are still getting a 6 minute egg! :[

So you need to edit ViewController.swift to use the stored value for the timing and to listen for the Notification of change so the timer can be changed or reset.

Add this extension to ViewController.swift outside any existing class definition or extension - it groups all the preferences functionality into a separate package for neater code:

extension ViewController {

  // MARK: - Preferences

  func setupPrefs() {
    updateDisplay(for: prefs.selectedTime)

    let notificationName = Notification.Name(rawValue: "PrefsChanged")
    NotificationCenter.default.addObserver(forName: notificationName,
                                           object: nil, queue: nil) {
      (notification) in
      self.updateFromPrefs()
    }
  }

  func updateFromPrefs() {
    self.eggTimer.duration = self.prefs.selectedTime
    self.resetButtonClicked(self)
  }

}

This will give errors, because ViewController has no object called prefs. In the main ViewController class definition, where you defined the eggTimer property, add this line:

  var prefs = Preferences()

Now PrefsViewController has a prefs object and so does ViewController - is this a problem? No, for a couple of reasons.

  1. Preferences is a struct, so it is value-based not reference-based. Each View Controller gets its own copy.
  2. The Preferences struct interacts with UserDefaults through a singleton, so both copies are using the same UserDefaults and getting the same data.

At the end of the ViewController viewDidLoad function, add this call which will set up the Preferences connection:

    setupPrefs()

There is one final set of edits needed. Earlier, you were using hard-coded values for timings - 360 seconds or 6 minutes. Now that ViewController has access to Preferences, you want to change these hard-coded 360's to prefs.selectedTime.

Search for 360 in ViewController.swift and change each one to prefs.selectedTime - you should be able to find 3 of them.

Build and run the app. If you have changed your preferred egg time earlier, the time remaining will display whatever you chose. Go to Preferences, chose a different time and click OK - your new time will immediately be shown as ViewController receives the Notification.

PrefsUpdating

Start the timer, then go to Preferences. The countdown continues in the back window. Change your egg timing and click OK. The timer applies your new time, but stops the timer and resets the counter. This is OK, I suppose, but it would be better if the app warned you this was going to happen. How about adding a dialog that asks if that is really what you want to do?

In the ViewController extension that deals with Preferences, add this function:

  func checkForResetAfterPrefsChange() {
    if eggTimer.isStopped || eggTimer.isPaused {
      // 1
      updateFromPrefs()
    } else {
      // 2
      let alert = NSAlert()
      alert.messageText = "Reset timer with the new settings?"
      alert.informativeText = "This will stop your current timer!"
      alert.alertStyle = .warning

      // 3
      alert.addButton(withTitle: "Reset")
      alert.addButton(withTitle: "Cancel")

      // 4
      let response = alert.runModal()
      if response == NSApplication.ModalResponse.alertFirstButtonReturn {
        self.updateFromPrefs()
      }
    }
  }

So what's going on here?

  1. If the timer is stopped or paused, just do the reset without asking.
  2. Create an NSAlert which is the class that displays a dialog box. Configure its text and style.
  3. Add 2 buttons: Reset & Cancel. They will appear from right to left in the order you add them and the first one will be the default.
  4. Show the alert as a modal dialog and wait for the answer. Check if the user clicked the first button (Reset) and reset the timer if so.

In the setupPrefs method, change the line self.updateFromPrefs() to:

self.checkForResetAfterPrefsChange()

Build and run the app, start the timer, go to Preferences, change the time and click OK. You will see the dialog and get the choice of resetting or not.

Sound

The only part of the app that we haven't covered so far is the sound. An egg timer isn't an egg timer if is doesn't go DINGGGGG!.

In part 2, you downloaded a folder of assets for the app. Most of them were images and you have already used them, but there was also a sound file: ding.mp3. If you need to download it again, here is a link to the sound file on its own.

Drag the ding.mp3 file into the Project Navigator inside the EggTimer group - just under Main.storyboard seems a logical place for it. Make sure that Copy items if needed is checked and that the EggTimer target is checked. Then click Finish.

AddFile

To play a sound, you need to use the AVFoundation library. The ViewController will be playing the sound when the EggTimer tells its delegate that the timer has finished, so switch to ViewController.swift. At the top, you will see where the Cocoa library is imported.

Just below that line, add this:

import AVFoundation  

ViewController will need a player to play the sound file, so add this to its properties:

var soundPlayer: AVAudioPlayer?

It seems like a good idea to make a separate extension to ViewController to hold the sound-related functions, so add this to ViewController.swift, outside any existing definition or extension:

extension ViewController {

  // MARK: - Sound

  func prepareSound() {
    guard let audioFileUrl = Bundle.main.url(forResource: "ding",
                                             withExtension: "mp3") else {
      return
    }

    do {
      soundPlayer = try AVAudioPlayer(contentsOf: audioFileUrl)
      soundPlayer?.prepareToPlay()
    } catch {
      print("Sound player not available: \(error)")
    }
  }

  func playSound() {
    soundPlayer?.play()
  }

}

prepareSound is doing most of the work here - it first checks to see whether the ding.mp3 file is available in the app bundle. If the file is there, it tries to initialize an AVAudioPlayer with the sound file URL and prepares it to play. This pre-buffers the sound file so it can play immediately when needed.

playSound just sends a play message to the player if it exists, but if prepareSound has failed, soundPlayer will be nil so this will do nothing.

The sound only needs to be prepared once the Start button is clicked, so insert this line at the end of startButtonClicked:

prepareSound()

And in timerHasFinished in the EggTimerProtocol extension, add this:

playSound()

Build and run the app, choose a conveniently short time for your egg and start the timer. Did you hear the ding when the timer ended?

Contributors

Zoltán Matók

Tech Editor

Chris Belanger

Editor

Michael Briscoe

Final Pass Editor and Team Lead

Over 300 content creators. Join our team.