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
In Part 1, you learned how to install Xcode and how to create a simple app. In Part 2, you created the user interface for a more complex app, but it doesn’t work yet as you have not coded anything. In this part, you are going to add the Swift code that will make your app come to life!
Getting Started
If you haven’t completed Part 2 or want to start with a clean slate, you can download the project files with the app UI laid out as it was at the end of Part 2. Open this project or your own project from Part 2 and run it to confirm that the UI is all in place. Open the Preferences window to check it as well.
Sandboxing
If you are an iOS developer, you will already be familiar with this concept – if not, read on.
A sandboxed app has its own space to work in with separate file storage areas, no access to the files created by other apps and limited access and permissions. For iOS apps, this is the only way to operate. For macOS apps, this is optional; however, if you want to distribute your apps through the Mac App Store, they must be sandboxed. As a general rule, you should keep your apps sandboxed, as this gives your apps less potential to cause problems. Starting with Xcode 12, this is enabled by default.
To view or modify sandboxing for the Egg Timer app, select the project in the Project Navigator — this is the top entry with the blue icon. Select EggTimer in the Targets list (there will only be one target listed), then click Signing & Capabilities in the tabs across the top. The display will expand to show the various permissions you can now request for your app. This app doesn’t need any of these, so leave them all unchecked.
Organizing Your Files
Look at the Project Navigator. All the files are listed with no particular organization. This app will not have very many files, but grouping similar files together is good practice and allows for more efficient navigation, especially with larger projects.
Select the two view controller files by clicking on one and Shift-clicking on the next. Right-click and choose New Group from Selection from the popup menu. Name the new group View Controllers.
The project is about to get some model files, so select the top EggTimer group, right-click and choose New Group. Call this one Model.
Drag the groups and files around until your Project Navigator looks like this:
MVC
This app is using the MVC pattern: Model View Controller.
The main model object type for the app is going to be a class called EggTimer
. This class will have properties for the start time of the timer, the requested duration and the elapsed time. It will also have a Timer
object that fires every second to update itself. Methods will start, stop, resume or reset the EggTimer
object.
The EggTimer
model class holds data and performs actions, but has no knowledge of how this is displayed. The Controller (in this case ViewController
), knows about the EggTimer
class (the Model) and has a View
that it can use to display the data.
To communicate back to the ViewController
, EggTimer
uses a delegate protocol. When something changes, the EggTimer
sends a message to its delegate
. The ViewController
assigns itself as the EggTimer's delegate
, so it is the one that receives the message and then it can display the new data in its own View.
Coding the EggTimer
Select the Model group in the Project Navigator and choose File/New/File… Select macOS/Swift File and click Next. Give the file a name of EggTimer.swift and click Create to save it.
Add the following code:
class EggTimer {
var timer: Timer? = nil
var startTime: Date?
var duration: TimeInterval = 360 // default = 6 minutes
var elapsedTime: TimeInterval = 0
}
This sets up the EggTimer
class and its properties. TimeInterval
really means Double
, but is used when you want to show that you mean seconds.
The next thing is to add two computed properties inside the class, just after the previous properties:
var isStopped: Bool {
return timer == nil && elapsedTime == 0
}
var isPaused: Bool {
return timer == nil && elapsedTime > 0
}
These are convenient shortcuts that can be used to determine the state of the EggTimer
.
Insert the definition for the delegate protocol into the EggTimer.swift file but outside the EggTimer
class – I like to put protocol definitions at the top of the file, after the import.
protocol EggTimerProtocol {
func timeRemainingOnTimer(_ timer: EggTimer, timeRemaining: TimeInterval)
func timerHasFinished(_ timer: EggTimer)
}
A protocol sets out a contract and any object that is defined as conforming to the EggTimerProtocol
must supply these 2 functions.
Now that you have defined a protocol, the EggTimer
can get an optional delegate
property which is set to any object that conforms to this protocol. EggTimer
does not know or care what type of object the delegate is, because it is certain that the delegate has those two functions.
Add this line to the existing properties in the EggTimer
class:
var delegate: EggTimerProtocol?
Starting the EggTimer
‘s timer object will fire off a function call every second. Insert this code which defines the function that will be called by the timer.
func timerAction() {
// 1
guard let startTime = startTime else {
return
}
// 2
elapsedTime = -startTime.timeIntervalSinceNow
// 3
let secondsRemaining = (duration - elapsedTime).rounded()
// 4
if secondsRemaining <= 0 {
resetTimer()
delegate?.timerHasFinished(self)
} else {
delegate?.timeRemainingOnTimer(self, timeRemaining: secondsRemaining)
}
}
So what's happening here?
-
startTime
is anOptional Date
- if it isnil
, the timer cannot be running so nothing happens. - Re-calculate the
elapsedTime
property.startTime
is earlier than now, so timeIntervalSinceNow produces a negative value. The minus sign changes it so that elapsedTime is a positive number. - Calculate the seconds remaining for the timer, rounded to give a whole number of seconds.
- If the timer has finished, reset it and tell the
delegate
it has finished. Otherwise, tell thedelegate
the number of seconds remaining. Asdelegate
is an optional property, the ? is used to perform optional chaining. If thedelegate
is not set, these methods will not be called but nothing bad will happen.
You will see an error until you add the final bit of code needed for the EggTimer
class: the methods for starting, stopping, resuming and resetting the timer.
// 1
func startTimer() {
startTime = Date()
elapsedTime = 0
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
self.timerAction()
}
timerAction()
}
// 2
func resumeTimer() {
startTime = Date(timeIntervalSinceNow: -elapsedTime)
timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
self.timerAction()
}
timerAction()
}
// 3
func stopTimer() {
// really just pauses the timer
timer?.invalidate()
timer = nil
timerAction()
}
// 4
func resetTimer() {
// stop the timer & reset back to start
timer?.invalidate()
timer = nil
startTime = nil
duration = 360
elapsedTime = 0
timerAction()
}
What are these functions doing?
-
startTimer
sets the start time to now usingDate()
and sets up the repeatingTimer
. -
resumeTimer
is what gets called when the timer has been paused and is being re-started. The start time is re-calculated based on the elapsed time. -
stopTimer
stops the repeating timer. -
resetTimer
stops the repeating timer and sets the properties back to the defaults.
All these functions also call timerAction
so that the display can update immediately.