We have a custom Swift type that can be used as the key type of dictionaries. We want to make an instance property of this type key-value observable as well. How do we do that?
Our Goal
In a previous post, I explained how to make a simple Student
class hashable. Here is the Student
class again.
class Student: Hashable { let studentID: Int var firstName: String var lastName: String var hashValue: Int { return self.studentID } init(studentID: Int, firstName: String, lastName: String) { self.studentID = studentID self.firstName = firstName self.lastName = lastName } } func ==(student1: Student, student2: Student) -> Bool { return student1.studentID == student2.studentID }
The following unit tests shows what we want to achieve.
func testResultObserver() { // [1] let larry = Student(studentID: 170523, firstName: "Larry", lastName: "Palmer") let mike = Student(studentID: 251943, firstName: "Mike", lastName: "Miller") let steve = Student(studentID: 184066, firstName: "Steve", lastName: "Baldwin") // [2] let resultSubject = ResultSubject() let resultObserver = ResultObserver() resultSubject.addObserver(resultObserver, forKeyPath: "fastestStudent", options: [.New], context: nil) XCTAssertEqual(resultObserver.currentLeader, "") // [3] resultSubject.updateResult(larry, time: 10.59) XCTAssertEqual(resultObserver.currentLeader, "Larry Palmer") // [4] resultSubject.updateResult(mike, time: 10.24) XCTAssertEqual(resultObserver.currentLeader, "Mike Miller") // [5] resultSubject.updateResult(steve, time: 10.27) XCTAssertEqual(resultObserver.currentLeader, "Mike Miller") // [6] resultSubject.updateResult(larry, time: 10.15) XCTAssertEqual(resultObserver.currentLeader, "Larry Palmer") // [7] resultSubject.removeObserver(resultObserver, forKeyPath: "fastestStudent") }
In block [1], we create three students who participate in a 100-metre dash.
In block [2], we create an instance of ResultSubject
, which stores the results of the students’ runs and plays the role of the Subject participant in the Observer pattern. ResultSubject
provides a dynamic or key-value-observable instance property fastestStudent
that holds the currently fastest student.
ResultSubject
notifies ResultObserver
about changes of fastestStudent
. ResultObserver
, which plays the ConcreteObserver of the Observer pattern, subscribes to the changes of fastestStudent
by adding its instance as an observer of fastestStudent
to resultSubject
. It stores the name of the fastest student in its string property currentLeader
. Initially, currentLeader
is empty.
As the students make their 100-metre runs (see blocks [3] to [6]), their results get entered into resultSubject
. If a student is faster than the currently fastest student, the currentLeader
gets updated. If not, the currentLeader
remains unchanged (see block [5]).
In block [7], we tell resultSubject
to stop notifiying resultObserver
about changes of fastestStudent
. If we forget to call removeObserver
, our app will crash as soon as resultObserver
goes out of scope.
Our goal is to make the class Student
key-value-observable and to show key-value observation in action by implementing ResultSubject
and ResultObserver
.
Making Student Key-Value-Observable
We make a Swift class key-value-observable by implementing the NSKeyValueObserving
protocol. We can avoid the pretty cumbersome implementation of this protocol by deriving the Student
class from NSObject
, which conforms to the NSKeyValueObserving
protocol already. Inheriting from NSObject
also conforms to the Hashable
and Equatable
protocol – a nice side effect.
The hashable and key-value-observable version of the class Student
looks as follows.
import Foundation class Student: NSObject { let studentID: Int var firstName: String var lastName: String override var hashValue: Int { return self.studentID } init(studentID: Int, firstName: String, lastName: String) { self.studentID = studentID self.firstName = firstName self.lastName = lastName } } func ==(student1: Student, student2: Student) -> Bool { return student1.studentID == student2.studentID }
There are only three small changes (shown in bold face above) over the original version. The class Student
now inherits from NSObject
instead of Hashable
. Using NSObject
requires us to import the Foundation
framework.
As NSObject
already defines hashValue
, we must override hashValue
in our implementation. Overriding a property only works for computed properties. It does not work for stored properties.
This was pretty easy. Now, let’s see our hashable and key-value-observable class in action.
Key-Value Observation in Action: ResultSubject and ResultObserver
ResultSubject
stores the results of the 100-metre runs in a dictionary mapping students to their times. The function udpateResult
stores the time
for a student
in the result dictionary and updates the fastestStudent
if the student
is faster than the students before. Here is the complete code of ResultSubject
.
import Foundation class ResultSubject: NSObject { dynamic var fastestStudent: Student? private var results: [Student: Double] = [:] func updateResult(student: Student, time: Double) { self.results[student] = time if self.fastestStudent == nil || time < self.results[self.fastestStudent!] { self.fastestStudent = student } } }
ResultSubject
must inherit from NSObject
to make it observable. NSObject
provides the functions addObserver
and removeObserver
for adding and removing observers.
We make an instance property observable by marking it with the dynamic
keyword. Of course, the dynamic property must also implement the NSKeyValueObserving
protocol as it is the case for NSObject
. Hence, the property fastestStudent
is key-value-observable. Whenever we assign a value to fastestStudent
, ResultSubject
notifies all registered observers about the new value. The above implementation makes sure that the notification only happens when the value changes.
The observer ResultObserver
of the results of the 100-metre dash must implement the function observeValueForKeyPath
. ResultSubject
calls this function on every observer when the value of an observed property changes. Here is the complete code.
import Foundation class ResultObserver: NSObject { var currentLeader: String = "" override func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer) { if keyPath == "fastestStudent" { if let student = change?[NSKeyValueChangeNewKey] as? Student { self.currentLeader = student.firstName + " " + student.lastName print("The new leader is \(self.currentLeader)") } } } }
The parameter keyPath
contains the name of the observed property - "fastestStudent"
in our example.
The parameter object
holds a reference to the observed object - ResultSubject
in our example. I recommend not to use the object
parameter, because it nullifies the very idea of the Observer pattern. The Observer pattern decouples the observed object from its observers. The observers should not know anything about the observed object. In our example, ResultObserver
does not refer to ResultSubject
at all - perfect decoupling.
By the way, ResultSubject
does not know anything of ResultObject
as well. It only knows that ResultObject
is an NSObject
. It only uses the NSObject
part of ResultObject
to notify observers about changes.
The parameter change
is a dictionary containing the old and new value of the observed property. In our example, we are only interested in the new value change?[NSKeyValueChangeNewKey]
.
If ResultObserver
receives a change of the "fastestStudent"
property, it assigns the first and last name of the new fastest student to the instance variable currentLeader
. The name of the currentLeader
would be shown in the UI of a real-life app. In our example app, we simply print the name.
The connection between the observed object ResultSubject
and the observer ResultObserver
is established by calling the function addObserver
.
resultSubject.addObserver(resultObserver, forKeyPath: "fastestStudent", options: [.New], context: nil)
From then on, every change of the property fastestStudent
of ResultSubject
is reported to ResultObserver
.