iOS Communication Patterns Explained – Part 3: KVO, Key-Value Observing

This post has been updated, based on quellish recommendations, made on reddit.

At the previous post of this series we made some changes of the code, in order to use the NSNotificationCenter for the asynchronous communication between the different parts of our system. NSNotification centre is a great tool if you want to set up 1 to N communication, and it is advised to use primarily in the communication which flows from the model to the controller in the Model-View-Controller pattern.

In this post I will change the communication to another custom implementation of the observer software design pattern, which is called Key-Value Observing, or KVO. If I want to describe the main functionality of the KVO I would say, that an object (or more) is going to keep an eye (observe) of one (or more) of the class properties. If this property changed, our observer class will do certain action. The main concept behind this communication pattern is the Key-Value Coding. In a nutshell Key-Value Coding enables you to use valueForKey and setValue: forKey: methods, if you implement NSKeyValueCoding protocol. If our class implements this protocol, then our class is KVC compliant. NSObject class is KVC compliant, so if you use it as a superclass for your classes, you already implemented the aforementioned protocol. More about the KVC: https://developer.apple.com/library/content/documentation/General/Conceptual/DevPedia-CocoaCore/KeyValueCoding.html

In our example I will add a public property to our downloader, called downloadedData which will contain the downloaded data. Our controller will observe this particular property of the downloader. If downloadedData has changed that means that the downloader finished with downloading the data into this property.

How to implement

What do you need to implement the KVO:
– Setting up the observation. It is quite similar what we did with the Notification Center, but now our observer will only watch for a given value (property) changes.
– Implementing - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change: in the same class, which is observing the property.
– If you haven’t overridden the default setter method, and your class is a subclass of NSObject, you have nothing to do. Otherwise you have to implement the willChangeValueForkey and didChangeValueForKey to trigger the observable event.

A little refactoring, again…

In order to have implement the Key-Value observing we need to make some changes in or class relationship:
kvo

As you can see now the PMODownloader relation with PMOPictureController has changed. The main difference is that now the PMOPictureController contains the downloader, which means that we need to have a strong relationship between those classes. This type of communications unfortunately does not really help the loose coupling.

As a side note, think a little bit about the case when the download fails. Since we are going to observe the value, which changes only when its content is successfully downloaded, we can not use the KVO for detecting the download errors. In order to still have this opportunity, I will leave the failure code to still use the Notification Center.

Change our code

Since we want to store the downloaded data and observe the changes by other classes in the PMODownloder class, we need to have a public property for that, and we have to also modify the implementation of this class.

PMODownloader.h

//1 As you can see I defined a new, public property, called downloadedData. This property will be the subject of the observation from our PMOPictureController.

PMODownloader.m

//2 In most of the cases the self.downloadedData = data; would be enough, but since I created a custom setter method, we need to use the willChangeValueForKey: and didChangeValueForKey: methods to trigger an observing event in the setter method, see below.

At //3 I created an accessor method for the previously defined property with the lazy instantiation. As I mentioned above the setter here will be custom, that’s why I used the willChangeValueForKey:, didChangeValueForKey methods. This is the most important part in our case, which covers the third point of the necessary steps to have a KVO.

PMOPictureController.m

Thanks for our design we need only change the implementation file of PMOPictureController.

I added a new pointer at //1, which will identify the context of the change. I will explain a bit more below.

At //2 we need to define our downloader class as a strong property. I did this because I need to have a reference to this object in order to remove the observer from the value in more than one different methods.

At //3 I initialise the downloader, and set up both of the observation right after that. I created a new method, called addObserverForKeyValueObservationDownloader, and in this method I am setting up the actual KVO observation.

The downloadImage method at //3 changed a bit, I am just calling the actual download routine from the downloader. I also removed the didImageDownloaded: method, since there is no need anymore for this method.

At //4 I removed the - (void)didImageDownloaded:(NSNotification *)notification method, since we won’t need it anymore. Instead of that we have - (void)addObserverForKeyValueObservationDownloader:(PMODownloader *)downloader method which contains the first of the three requested steps to the KVO: we are applying the observer pattern on the downloadedData property. Please note that we are using the context pointer, defined at //1. Since we can observe more than one value, and the method triggered for those events are same, context will help us to easily identify, which KVO event we want to respond.

At //5 there is the last piece of the three requirements, mentioned in the requirements. The - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context method is the last piece of our puzzle. Actually you can think about a callback method, for each and any observed KVO value. It is s bit tricky, because we can even observe more than one value, and it will still trigger this particular method. That’s why I created the pointer at //1 and used this pointer as a context at the init time, to easily identify the KVO event.

At //6 I removed the observer for the Notification Center in that case when the download was successful, but I kept the failure one.

//7 is about removing the observers. As the downloader initialised at the init time, that method should be called only once, when the object is deallocated.

At //8 there is the dealloc method, where we are cleaning up after ourselves. Removing the observers, and set the downloader nil, in order to avoid from retain cycle.

Tests

I added a dedicated test for the PMODownloader class:
PMODownloaderTests.m

It is worth to mention to take a look at the testDownload method. We can easily set up the similar XCTestExpectation for the Key-Value Observing, as we did for the Notification Center. I also implemented a test for the Notification Center, to catch and test download errors.

PMOPictureControllerTests.m

I needed to change the method for the download notification. Actually at testPictureAsyncDownload I am checking an kind of masked property. The PMOPictureController’s image property is actually getting back the object’s PMOPictureWithURL’s image. Which means that the PMOPictureController’s image property is not a real property, a kind of calculated one, but with the technique willChangeValueForkey and didChangeValueForKey above we can easily trigger the KVO event.

Wrap up

As you can see this approach is bit closed, in terms that the involved classes could have a quite good knowledge from each other. That means that it is not loosely coupled, and it might still better to use in the 1:N communication scenarios. We don’t need to prepare extra payload for passing to the changed values though, since it is the part of the mechanism. For the scenario above, I still wouldn’t consider as a good solution, but can be very useful in the Controller-View communication.

Next time we are going to approach the same problem with the delegation pattern.


Leave a Reply