Showing posts with label KVO. Show all posts
Showing posts with label KVO. Show all posts

Tuesday, November 24, 2009

Using KVO for Table Updates

If you've followed the guidelines in Apple's Model Object Implementation Guide when creating your data model objects, you can handle your UITableView updates using KVO. This frees you from having to spread calls to reloadData, insertRowsAtIndexPaths:withRowAnimation:, or deleteRowsAtIndexPaths:withRowAnimation: throughout your controller class wherever the data in your table might get changed.

Instead, all you have to do is observe the keypath on your data model object that holds the collection of items being displayed in the table. The easiest way to do that is in your table view controller's viewDidLoad method, like so:

    [self.data addObserver:self
forKeyPath:@"items"
options:0
context:NULL
]
;

Then, you just implement observeValueForKeyPath:ofObject:change:context to insert or remove rows based on changes to the observed collection. The information about which rows were inserted or deleted comes in the changes dictionary stored under the key NSKeyValueChangeIndexesKey. The information comes in the form of index sets, and those have to be converted to index paths in order to update the table. But that's the only thing about this code that isn't fairly straightforward, and this implementation is fairly generic, so you should be able to pretty much copy and paste it into your controller if you want to use it.

If you do do this, make sure you remove all the other remove and insert calls, otherwise you will double-delete and double-insert, which will cause errors at runtime.

- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
NSIndexSet *indices = [change objectForKey:NSKeyValueChangeIndexesKey];
if (indices == nil)
return; // Nothing to do


// Build index paths from index sets
NSUInteger indexCount = [indices count];
NSUInteger buffer[indexCount];
[indices getIndexes:buffer maxCount:indexCount inIndexRange:nil];

NSMutableArray *indexPathArray = [NSMutableArray array];
for (int i = 0; i < indexCount; i++) {
NSUInteger indexPathIndices[2];
indexPathIndices[0] = 0;
indexPathIndices[1] = buffer[i];
NSIndexPath *newPath = [NSIndexPath indexPathWithIndexes:indexPathIndices length:2];
[indexPathArray addObject:newPath];
}


NSNumber *kind = [change objectForKey:NSKeyValueChangeKindKey];
if ([kind integerValue] == NSKeyValueChangeInsertion) // Rows were added
[self.tableView insertRowsAtIndexPaths:indexPathArray withRowAnimation:UITableViewRowAnimationFade];
else if ([kind integerValue] == NSKeyValueChangeRemoval) // Rows were removed
[self.tableView deleteRowsAtIndexPaths:indexPathArray withRowAnimation:UITableViewRowAnimationFade];

}

Once you've done this, you can forget about reloading your table. Any changes made to the underlying data - even changes made on other threads (assuming your data model class is thread-safe) will automatically trigger an animated deletion or insertion. Although the code above may look a little intimidating, this actually allows a much cleaner overall design.

I've created a fairly simple Xcode project that illustrates using KVO to update a table view.

Wednesday, February 4, 2009

KVO and the iPhone SDK

One of the common misconceptions about the iPhone SDK is that it doesn't support Key-Value Observation, or KVO. I suspect that the reason for this misconception is that the most conspicuous uses of KVO in Cocoa—Core Data and Bindings—has not been brought to the iPhone.

Now, KVO is not used much in the iPhone SDK. I don't think we ever mentioned it in the book. But it is available. The NSKeyValueObservation informal protocol is part of the iPhone SDK. It's completely opt-in on the iPhone, however. When you create an application project in Xcode, none of your objects are observing any other. Well, there might be some KVO being used behind the scenes (in fact, I suspect there is), but most people won't encounter it.

But, any Objective-C object can register to receive notifications when a property of another object is changed. An object can even observe itself. Here is how you register to observe an object. In this example, theObject is being told to notify theObserver any time the property called name

[theObject addObserver:theObserver forKeyPath:@"name" options:0 context:nil];

Now, theObserver just has to implement this method to find out when theObject.name changes:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{

}

This is really quite a flexible bit of functionality. When registering as an observer, you can use the options: argument to tell the KVO system whether you want the old value, the new value, or the original value of the property being changed. You can even use keypaths, not just keys. So, if name weren't a string, but were actually another object with component string properties called firstName and lastName, we could pass in a key path of @"name.firstName" to indicate that we want to get notified when the firstName property of name changes.

This is some pretty cool stuff. I used this functionality recently in SQLitePersistentObjects to set the dirty flag on objects so that they only get saved when something has changed since it was loaded, or the last time it was saved. In the init method, I simply iterate through the properties of the object (including those declared in superclasses), and for any property that isn't read-only, I register it as an observer of itself. Then, when the object gets notified of any change, I mark the object as dirty, which lets me know to actually save the data when the save method is called.

In the future, I may implement a more sophisticated version that only updates the actual properties that are dirty rather than saving the whole object.