An oft forgotten feature of Cocoa and Cocoa Touch development is Key-Value Observing, or KVO. It’s an ingenious system that lets us notify one class if something changes in another class. Or more specifically, if an object’s property changes in one instance, another object can react to it.
This sounds way more complicated than it really is. Think of all those times when you write a protocol, or create an NSNotification to tell another class to do something. Both those approaches are valid, and sometimes necessary, but there is a simpler alternative that I often forget when I’m hacking away.
Here’s how it works, shown with a simple example.
If Device A saves or changes a file in your app’s iCloud folder, Device B might want to know about this. So you’d imagine there should be an easy way to monitor those changes by way of a notification or a delegate. Sadly there isn’t. Like many things in Objective C, this is way more complicated than it needs to be.
Sure, we could use UIDocument which has those notifications built in – but that adds a layer of complexity to the simple act of saving a frigging file that many apps seriously don’t need.
The good news is that we can monitor file changes to any file in our ubiquitous folder by executing an NSMetadataQuery. It feels like a hack but it works. Let me explain.
An NSMetadataQuery is really a way to search files in a folder and return results. This includes iCloud documents. As a byproduct it sends notifications if there have been changes to those results since the last query. Queries are ongoing until stopped and are carried out once every second.
So to monitor changes to files in our iCloud folder, we need to do the following:
- create an NSMetadataQuery, monitoring all files in our ubiquitous Documents folder (including new save, overwrite and delete)
- add an observer for the query
- create a method that gets fired via the observer
Let’s do this
I’ve been battling with those AppDelegate methods for a while, thinking “I wish there was a way that I don’t have to use those methods”. And guess what: there is!
Instead of adding code where it doesn’t make sense, we can listen to a system wide notification that tells us our app has come into the foreground, like so:
// observer checks if we're back from the background
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(myMethod) name:UIApplicationWillEnterForegroundNotification object:nil];
Here the selector “myMethod” is called when our app is about to enter the foreground. Some of the interesting notifications are
Find many more in Apple’s UIApplication Class Reference:
Now that we can read from and write to iCloud, how will we know when another device may have written new data?
We’ll do this by adding an observer and listening to a special message iCloud transmits every time new data is available. The message has the catchy title NSUbiquitousKeyValueStoreDidChangeExternallyNotification.
Here’s how we can setup such an observer. This calls a method (newCloudData) in which you can react to the changes:
// setup an observer
[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(newCloudData) name:NSUbiquitousKeyValueStoreDidChangeExternallyNotification object:nil];
Observers are a great way to call a method in a class when you don’t have direct access to it. If you can’t figure out what relationship this class has to the one you want to call the method from, Observers are a very easy way to send a message and trigger an action.
In the Master/Detail template for example, the Master View can update the detail view due to the View Controller relationship. But the other way round isn’t so easy – that’s where an Observer can come in handy.
First we setup the Observer in the class which contains the method we’d like to use:
// add observer
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(myMethod) name:@"myMessage" object:nil];
The viewDidLoad method is a good place for this if it’s available. The two important parameters here are the selector which needs to be set to your existing method, and the name which is an arbitrary name that the observer listens to. You may also remove the observer when you’re done, perhaps in the viewDidUnload method (where available):
// remove observer
Next we’ll call the Observer from the other class like this:
NSNotification *notification = [NSNotification notificationWithName:@"myMessage" object:self];
This will trigger the myMethod method in your other class.