Skip to content

Making a Hashable Type Key-Value-Observable

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.

Leave a Reply

Your email address will not be published. Required fields are marked *