Cocoa for Scientists (Part XII): Observe and Learn

Author: Drew McCormack
Website: http://www.maccoremac.com

In preparation for our first foray into Cocoa Bindings, last time we got acquainted with key-value coding (KVC). The principle behind KVC is that you can access properties of objects using string keys and key paths, rather than making a direct call to an object accessor method. This is a powerful feature of Cocoa, and is one of the most important technologies underlying Interface Builder.

With KVC, you can set and get values in an object, but for bindings we need one more ingredient: key-value observing (KVO). KVO is used to observe an attribute in an object, and get notifications when it changes. With KVC and KVO, it is possible to build general controller classes to keep the user interface in synch with the model objects (remember MVC?). Cocoa Bindings is nothing more than a set of these general controller classes.

Key-Value Observing

You don’t need to know much about KVO to use Cocoa Bindings, but you should know it exists, and know a few basic principles. You can always dig deeper later.

To use Cocoa Bindings, you need to make sure that the model classes that you write work with KVO. This is actually much simpler than it sounds, because it simply involves sticking to the standard Cocoa method naming conventions. If your classes work with KVC, they will also work with KVO.

For example, last week we introduced a Molecule class, that contained an array of Atom objects:

@interface Atom : NSObject {
    float mass;
}
-(void)setMass:(float)newMass;
-(float)mass;
@end

@interface Molecule : NSObject {
    NSArray *atoms;
}
-(void)setAtoms:(NSArray *)newAtoms;
-(NSArray *)atoms;
@end

As you can see, I have included accessor methods for the atoms array. This is good practice, because it makes memory management simpler and encapsulates the atoms array. But when you write Cocoa-conforming accessor methods like these you also get KVO for free. To see how this works, imagine that a controller class wanted to monitor when a particular Molecule object’s atoms property changed, so that it could then update a table of atom masses in the user interface. The controller class could register with the Molecule to observe changes in the atoms property, like so

Molecule *molecule = [Molecule new];
[molecule addObserver:self forKeyPath:@"atoms" options:0 context:NULL];

The above would typically be found in an initializer method, or awakeFromNib, so that it is executed when the controller object is first created. I have assumed that this code appears in the controller class, and that self is the controller instance. The options allow you to stipulate extra information to be included when the controller is notified of any changes, but usually you just set it to zero. Lastly, there is an optional context argument. This is also passed along when the notification occurs. Most of the time you don’t need it, and can simply pass NULL.

When the controller no longer wants to observe the Molecule, it removes itself as an observer:

[molecule removeObserver:self forKeyPath:@"atoms"];

This invocation is usually found in a dealloc method.

This covers adding and removing observers, but what happens when a new atoms array is set? The controller method observeValueForKeyPath:ofObject:change:context: gets invoked in the controller class

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change 
    context:(void *)context {
    if ( [keyPath isEqualToString:@"atoms"] ) {
        // Update the UI with the new atom masses
        ...
    }
}

This NSObject method is a catch-all for KVO notifications, so you have to check the key path to determine what the notification relates to. In this case, we make sure that the notification received is for the atoms property before updating the user interface. The change dictionary has information about the changes that have occurred; the content of this dictionary depends on what options you pass to addObserver:forKeyPath:options:context:. Lastly, the context argument to addObserver:forKeyPath:options:context: is also passed back to this method; you can use it to pass a useful object or some state information, but most of the time you don’t need it.

KVO and Paths

You may recall from the last tutorial that KVC worked for key paths, as well as simple keys. This means you can traverse an object tree very simply, possibly getting or setting attributes on many objects, in a single line of code. No loops necessary.

The same applies to KVO. You can use key paths, and observe many objects with a single line of code. For example, if in the controller class you wanted to know whenever an atom mass was changed, you could use code like this to register for notifications:

[molecule addObserver:self forKeyPath:@"atoms.mass" options:0 context:NULL];

and code like this to observe the change

-(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change 
    context:(void *)context {
    if ( [keyPath isEqualToString:@"atoms.mass"] ) {
        // Do something with the new masses
        ...
    }
}

You will need to use key paths extensively when working with Cocoa Bindings.

Binding it All Together

That’s it for this time — a short introduction to KVO. Next week we will take what we have learned in the last two tutorials on KVC and KVO, return to the Unitary application, and start using Cocoa Bindings.

Comments

re: observe and learn

neat tutorial. thanks.

Still missing a point

I'm a Cocoa newbie and despite reading many times this tutorial, I don't understand where it is written in the class interface that NSArray *atoms is array containing Atom objects.

So it still obscure to me how this

[molecule addObserver:self forKeyPath:@"atoms.mass" options:0 context:NULL];

can work. This may sound like a stupid question, but I have not been able to find an answer on the web.

Array of atoms

This is not a silly question at all. It is quite common when coming from languages like C++ and Fortran to wonder such things when confronted with Objective-C. The difference between Objective-C and the other languages is that Objective-C is dynamically typed (with some static-typing extensions).

What this means is that when you use objects in Objective-C, it doesn't care what type they are. An array can hold Atoms or Molecules, or both. The only time the actual class of an object is important is when you send it a message, ie, invoke a method.

So an NSArray can hold anything: the compiler will not check what you put into it. But when you come to use the objects in the array, you will get a run-time exception if you have put in something that doesn't belong there. For example, if instead of Atom objects, you put Car objects in the array in the example above, your program would throw an exception when you tried to query its chemical element.

Does that help?

Drew

---------------------------
Drew McCormack
http://www.maccoremac.com
http://www.macanics.net
http://www.macresearch.org

Array of atoms

Yes this helps and this is now clearer, especially since I've mainly a Pascal background! Thanks!