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
Update note: Roberto Machorro updated this tutorial for Xcode 12 and Swift 5. Sarah Reichelt wrote the original article.

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?

  1. startTime is an Optional Date - if it is nil, the timer cannot be running so nothing happens.
  2. 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.
  3. Calculate the seconds remaining for the timer, rounded to give a whole number of seconds.
  4. If the timer has finished, reset it and tell the delegate it has finished. Otherwise, tell the delegate the number of seconds remaining. As delegate is an optional property, the ? is used to perform optional chaining. If the delegate 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?

  1. startTimer sets the start time to now using Date() and sets up the repeating Timer.
  2. 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.
  3. stopTimer stops the repeating timer.
  4. 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.

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.