Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

iOS Communication Patterns Explained, Part III: Key-Value Observing

DZone's Guide to

iOS Communication Patterns Explained, Part III: Key-Value Observing

See how to change communication to another custom implementation of the observer software design pattern: KVO. The main concept behind KVO is the Key-Value Coding.

· Mobile Zone ·
Free Resource

This post has been updated based on quellish recommendations made on Reddit.

Additionally, this post is the third part of a series. You can view Part I here and Part II here.

In the previous post of this series, we made some changes to the code in order to use the NSNotificationCenter for the asynchronous communication between the different parts of our system. NSNotification center 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 a 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. The 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 can be found here

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:

  • Set 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.

  • Implement  - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change: inin 
    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:
kvoAs 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

#import <Foundation/Foundation.h>
​
@interface PMODownloader : NSObject
​
//1
/**
Property to store the downloaded data in NSData format
*/
@property (strong, nonatomic, nullable) NSData *downloadedData;
/**
Downloading and giving back the raw data result from the url.
@param url the source url
*/
- (void)downloadDataFromURL:(nonnull NSURL *)url;
​
@end

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

#import "PMODownloader.h"
#import "PMODownloadNotifications.h"
​
@implementation PMODownloader
​
#pragma mark - Public API
- (void)downloadDataFromURL:(NSURL *)url {
​
NSURLRequest *request = [NSURLRequest requestWithURL:url];
NSURLSession *session = [NSURLSession sharedSession];
​
NSURLSessionDataTask *task = [session dataTaskWithRequest:request
completionHandler:
^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
[self notifyObserverDownloadFailure];
} else {
//2
self.downloadedData = data;
}
}];
[task resume];
​
}
​
//3
#pragma mark - Accessors
- (void)setDownloadedData:(NSData *)downloadedData {
[self willChangeValueForKey:@"downloadedData"];
if (!_downloadedData) {
_downloadedData = [[NSData alloc] init];
}
_downloadedData = downloadedData;
[self didChangeValueForKey:@"downloadedData"];
}
​
​
#pragma mark - Notifications
​
​
​
- (void)notifyObserverDownloadFailure {
​
[[NSNotificationCenter defaultCenter] postNotificationName:PMODownloadFailed
object:self
userInfo:nil];
}
​
​
@end

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

#import "PMOPictureController.h"
#import "PMODownloader.h"
#import "PMOPictureWithURL.h"
#import "PMODownloadNotifications.h"
//1
static void *DownloadedDataObservation = &DownloadedDataObservation;
​
@interface PMOPictureController()
​
//2
/**
Our private data class, storing and hiding the information.
*/
@property (strong, nonatomic, nullable) PMOPictureWithURL *pictureWithUrl;
​
/**
The downloader, which downloads the data. We need to keep it as a property as long as
we want to use the Key-Value Observation
*/
@property (strong, nonatomic, nullable) PMODownloader *downloader;
@end
​
@implementation PMOPictureController
​
//3
#pragma mark - Initializers
- (instancetype)initWithPictureURL:(NSURL *)url {
​
self = [super init];
if (self) {
_pictureWithUrl = [[PMOPictureWithURL alloc] initWithPictureURL:url];
_downloader = [[PMODownloader alloc] init];
[self addObserverForKeyValueObservationDownloader:_downloader];
[self addObserverForDownloadTaskWithDownloader];
}
return self;
}
​
​
#pragma mark - Public API
- (void)downloadImage {
//4
[self.downloader downloadDataFromURL:self.pictureWithUrl.imageURL];
}
​
#pragma mark - Accessors
- (UIImage *)image {
return self.pictureWithUrl.image;
}
​
​
#pragma mark - Notification Events
- (void)didImageDownloadFailed {
NSLog(@"Image download failed");
}
​
​
​
#pragma mark - Notification helpers
- (void)addObserverForKeyValueObservationDownloader:(PMODownloader *)downloader {
[downloader addObserver:self forKeyPath:@"downloadedData"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:DownloadedDataObservation];
​
}
​
//5
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context {
if (context == DownloadedDataObservation) {
[self willChangeValueForKey:@"image"];
self.pictureWithUrl.image = [UIImage imageWithData:self.downloader.downloadedData];
[self didChangeValueForKey:@"image"];
} else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
​
//6
- (void)addObserverForDownloadTaskWithDownloader {
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didImageDownloadFailed)
name:PMODownloadFailed
object:nil];
}
​
​
- (void)removeObserverForDownloadTask {
//7
[self.downloader removeObserver:self forKeyPath:@"downloadedData" context:DownloadedDataObservation];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
​
​
#pragma mark - Dealloc
//8
- (void)dealloc {
[self removeObserverForDownloadTask];
self.downloader = nil;
}
​
​
​
@end

Thanks to 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 initialize 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 it.

At //4, I removed the - (void)didImageDownloaded:(NSNotification *)notification method since we won’t need it anymore. Instead of that, we have the - (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 *)c hange context:(void *)contextmethodis the last piece of our puzzle. Actually, you can think about a callback method for each and any observed KVO value. It is a 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

​
#import <XCTest/XCTest.h>
#import "PMODownloader.h"
#import "PMODownloadNotifications.h"
​
@interface PMODownloaderTests : XCTestCase
@property (strong, nonatomic) PMODownloader *downloader;
@end
​
@implementation PMODownloaderTests
​
- (void)setUp {
[super setUp];
self.downloader = [[PMODownloader alloc] init];
}
​
- (void)tearDown {
self.downloader = nil;
[super tearDown];
}
​
/**
Testing download OK.
*/
- (void)testDownload {
XCTestExpectation *expectation = [self keyValueObservingExpectationForObject:self.downloader keyPath:@"downloadedData" handler:^BOOL(id _Nonnull observedObject, NSDictionary * _Nonnull change) {
[expectation fulfill];
return true;
}];
​
[self.downloader downloadDataFromURL:[NSURL URLWithString:@"https://upload.wikimedia.org/wikipedia/en/4/45/One_black_Pixel.png"]];
​
[self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) {
if (error) {
NSLog(@"Timeout Error: %@", error);
}
}];
}
​
/**
Testing the download failure wiht a nonexisting domain
*/
- (void)testPictureAsyncDownloadFailed {
​
​
​
XCTestExpectation *expectation = [self expectationForNotification:PMODownloadFailed
object: nil
handler:^BOOL(NSNotification * _Nonnull notification) {
[expectation fulfill];
return true;
}];
​
[self.downloader downloadDataFromURL:[NSURL URLWithString:@"https://ThereIsNoSuCHDomainName/MssedUPName.png"]];
[self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) {
if (error) {
NSLog(@"Timeout Error: %@", error);
}
}];
}
​
​
@end

It is worth mentioning 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

#import <XCTest/XCTest.h>
#import "PMOPictureController.h"
#import "PMODownloadNotifications.h"
​
@interface PMOPictureControllerTests : XCTestCase
@property (strong, nonatomic) PMOPictureController *pictureController;
@end
​
@implementation PMOPictureControllerTests
​
- (void)setUp {
[super setUp];
// Set up the controller with a valid image URL.
self.pictureController = [[PMOPictureController alloc] initWithPictureURL:[NSURL URLWithString:@"https://upload.wikimedia.org/wikipedia/en/4/45/One_black_Pixel.png"]];
}
​
- (void)tearDown {
// Being a good citizen, and nil out the strong reference, so we can safely initialize again a new instance
// for the next test
self.pictureController = nil;
[super tearDown];
}
​
​
/**
Test the asynchron download
Internet connection required to pass this test!
*/
- (void)testPictureAsyncDownload {
XCTestExpectation *expectation = [self keyValueObservingExpectationForObject:self.pictureController keyPath:@"image" handler:^BOOL(id _Nonnull observedObject, NSDictionary * _Nonnull change) {
[expectation fulfill];
return true;
}];
​
[self.pictureController downloadImage];
[self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) {
if (error) {
NSLog(@"Timeout Error: %@", error);
}
}];
}
​
/**
Testing the download failure wiht a nonexisting domain
*/
- (void)testPictureAsyncDownloadFailed {
​
self.pictureController = [[PMOPictureController alloc] initWithPictureURL:[NSURL URLWithString:@"https://ThereIsNoSuCHDomainName/MssedUPName.png"]];
​
XCTestExpectation *expectation = [self expectationForNotification:PMODownloadFailed
object: nil
handler:^BOOL(NSNotification * _Nonnull notification) {
[expectation fulfill];
return true;
}];
​
[self.pictureController downloadImage];
[self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) {
if (error) {
NSLog(@"Timeout Error: %@", error);
}
}];
}
​
​
​
@end

I needed to change the method for the download notification. Actually, at testPictureAsyncDownload, I am checking a 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. However, with the technique willChangeValueForkey and didChangeValueForKey above, we can easily trigger the KVO event.

Wraping Up

As you can see, this approach is bit closed in the 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 be 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 this a good solution, but it can be very useful in the Controller-View communication.

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

Topics:
mobile ,ios ,key value observing ,key value coding

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}