8.
Observer Pattern
Written by Joshua Greene
The observer pattern lets one object observe changes on another object. Apple added language-level support for this pattern in Swift 5.1 with the addition of Publisher in the Combine framework.
This pattern involves three types:
- The subscriber is the “observer” object and receives updates.
- The publisher is the “observable” object and sends updates.
- The value is the underlying object that’s changed.
Note: this chapter provides a high-level introduction to
@Publishedproperties, but it doesn’t get into all of the details or powerful features offered in theCombineframework. If you’d like to learn moreCombine, see our book Combine: Asynchronous Programming with Swift (http://bit.ly/swift-combine).
When should you use it?
Use the observer pattern whenever you want to receive changes made on another object.
This pattern is often used with MVC, where the view controller has subscriber(s) and the model has publisher(s). This allows the model to communicate changes back to the view controller without needing to know anything about the view controller’s type. Thereby, different view controllers can use and observe changes on the same model type.
Playground example
Open FundamentalDesignPattern.xcworkspace in the Starter directory, or continue from your own playground workspace from the last chapter, and then open the Overview page.
You’ll see Observer is listed under Behavioral Patterns. This is because Observer is about one object observing another object.
Click on the Observer link to open that page.
Then, enter the following below Code Example:
// 1
import Combine
// 2
public class User {
// 3
@Published var name: String
// 4
public init(name: String) {
self.name = name
}
}
Here’s what you did:
-
First, you import
Combine, which includes the@Publishedannotation andPublisher&Subscribertypes. -
Next, you declare a new
Userclass;@Publishedproperties cannot be used on structs or any other types besides classes. -
Next, you create a
varproperty fornameand mark it as@Published. This tells Xcode to automatically generate aPublisherfor this property. Note that you cannot use@Publishedforletproperties, as by definition they cannot be changed. -
Finally, you create an initializer that sets the initial value of
self.name.
Next, add the following code to the end of the playground:
// 1
let user = User(name: "Ray")
// 2
let publisher = user.$name
// 3
var subscriber: AnyCancellable? = publisher.sink() {
print("User's name is \($0)")
}
// 4
user.name = "Vicki"
Here’s what you did:
-
First, you create a new
usernamedRay. -
Next, you access the
publisherfor broadcasting changes to the user’s name viauser.$name. This returns an object of typePublished<String>.Publisher. This object is what can be listened to for updates. -
Next, you create a
subscriberby callingsinkon the publisher. This takes a closure for which is called for the initial value and anytime the value changes.By default,
sinkreturns a type ofAnyCancellable. However, you explicitly declare this type asAnyCancellable?to make it optional as you’ll nil it out later. -
Finally, you change the user’s name to
Vicki.
In response, you should see the following printed to the console:
User's name is Ray
User's name is Vicki
Add the following code next:
subscriber = nil
user.name = "Ray has left the building"
By setting the subscriber to nil, it will no longer receive updates from the publisher. To prove this, you change the user’s name a final time, but you won’t see any new output in the console.
What should you be careful about?
Before you implement the observer pattern, define what you expect to change and under which conditions. If you can’t identify a reason for an object or property to change, you’re likely better off not declaring it as var or @Published, and instead, making it a let property.
A unique identifier, for example, isn’t useful as an published property since by definition it should never change.
Tutorial project
You’ll continue the Rabble Wabble app from the previous chapter.
If you skipped the previous chapter, or you want a fresh start, open Finder and navigate to where you downloaded the resources for this chapter. Then, open starter ▸ RabbleWabble ▸ RabbleWabble.xcodeproj in Xcode.
You’ll use the observer pattern to display the user’s latest score on the “Select Question Group” screen.
Open QuestionGroup.swift from the File hierarchy. This already has a Score, but it’s not currently possible to observe changes on it. Add the following below import Foundation:
import Combine
This imports Apple’s new Combine framework to do all the heavy lifting for you.
Next, add the following to the end of the Score class (which is inside the QuestionGroup class) after its other properties, ignorning the compiler error for now:
@Published public var runningPercentage: Double = 0
The runningPercentage property will allow the question group’s latest “running percentage score” to be observed.
The compiler is currently throwing an error because the property is marked as @Published, and it doesn’t know how to automatically encode or decode it.
To fix this, add the following code right after init for Score:
// 1
private enum CodingKeys: String, CodingKey {
case correctCount
case incorrectCount
}
// 2
public required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.correctCount = try container.decode(Int.self, forKey: .correctCount)
self.incorrectCount = try container.decode(Int.self, forKey: .incorrectCount)
updateRunningPercentage()
}
// 3
private func updateRunningPercentage() {
let totalCount = correctCount + incorrectCount
guard totalCount > 0 else {
runningPercentage = 0
return
}
runningPercentage = Double(correctCount) / Double(totalCount)
}
Here’s what this does:
-
You first declare an enum for
CodingKeysand cases forcorrectCountandincorrectCount. This tells the compiler to ignorerunningPercentagein the encoder and decoder methods it automatically generates. -
In the event that a
Scoreis decoded, you need to actually setrunningPercentage. To do this, you create a custom initializer forinit(from decoder:), and callupdateRunningPercentage()after settingcorrectCountandincorrectCount. -
Within
updateRunningPercentage(), you set therunningPercentagebased on the ratio ofcorrectCounttototalCount.
Next, replace the var correctCount and var incorrectCount lines with the following:
public var correctCount: Int = 0 {
didSet { updateRunningPercentage() }
}
public var incorrectCount: Int = 0 {
didSet { updateRunningPercentage() }
}
While you could have marked correctCount and incorrectCount as @Published, you’re not interested in observing these properties individually. Rather, you’re interested in how they affect runningPercentage. So within didSet for each of these, you call updateRunningPercentage().
Before you can start creating subscribers for runningPercentage, you need to make a few small changes. First, add the following method to the end of the Score class before the closing curly brace:
public func reset() {
correctCount = 0
incorrectCount = 0
}
This method “resets” Score. You’ll use it whenever the user restarts a QuestionGroup.
Next, replace the var score line with the following, ignoring the resulting compiler error:
public private(set) var score: Score
This prevents all outside classes from setting score directly. This ensures any runningPercentage subscribers aren’t accidentally wiped out, which would happen if score was set directly.
There’s currently one place that does set score directly. Open BaseQuestionStrategy.swift and replace the following line:
self.questionGroupCaretaker.selectedQuestionGroup.score =
QuestionGroup.Score()
…with the following:
self.questionGroupCaretaker.selectedQuestionGroup.score.reset()
Build and run to ensure you don’t have any compiler errors. Nothing appears to have changed so far, but you’re now ready to register your observers!
You first need somewhere to hold onto the subscriber object. Ideally, this should be tied to the life of the object that it’s related. In this case, this is the QuestionGroupCell itself. Open QuestionGroupCell.swift and add the following below import UIKit:
import Combine
Next, add the following property after the others:
public var percentageSubscriber: AnyCancellable?
Then, open SelectQuestionGroupViewController.swift and add the following code to tableView(_:cellForRowAt:), just before the return statement:
cell.percentageSubscriber =
questionGroup.score.$runningPercentage // 1
.receive(on: DispatchQueue.main) // 2
.map() { // 3
return String(format: "%.0f %%", round(100 * $0))
}.assign(to: \.text, on: cell.percentageLabel) // 4
Here’s how this works:
-
Set
cell.percentageSubscriberto the subscriber that’s created. Consequently, if the cell gets released, its subscriber will automatically get released too, and it won’t receive updates. -
Call
receive(on:)and passDispatchQueue.mainto ensure events are delivered on the main queue. While there’s not any background threads currently used in the app, it’s a good idea to always ensure your UI calls are made on the main queue to prevent future issues. -
Transform the value into a percentage string using a
map. -
Call
assignto set the value totexton thecell.percentageLabel. Whenever the value changes, this will automatically update the label’s text too.
Build and run, pick any question group cell you’d like, and tap the “Correct” and “Incorrect” buttons a few times. When you press the “Menu” button, the score will now be visible. Even better, if you quit the app and restart it, the scores will be persisted thanks to your implementation of the memento pattern from the previous chapter.
Key points
You learned about the observer pattern in this chapter. Here are its key points:
-
The observer pattern lets one object observe changes on another object. It involves three types: the subscriber, publisher and value.
-
The subscriber is the observer; the publisher is the observable object; and the value is the object that’s being changed.
-
Swift 5.1 makes it easy to implement the observer pattern using
@Publishedproperties.
RabbleWabble is becoming ever more feature-rich. However, there’s one feature that would be really great: the ability for users to create their own QuestionGroups. You’ll use another pattern to do this: the builder design pattern.
Continue onto the next chapter to learn about the builder pattern and complete the RabbleWabble app.