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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
macOS Development for Beginners: Part 3
30 mins
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.
-
Preferences
is a struct, so it is value-based not reference-based. Each View Controller gets its own copy. -
The
Preferences
struct interacts withUserDefaults
through a singleton, so both copies are using the sameUserDefaults
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
.
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?
- If the timer is stopped or paused, just do the reset without asking.
-
Create an
NSAlert
which is the class that displays a dialog box. Configure its text and style. - 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.
- 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.
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?