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.



Friday, November 20, 2009

Private APIs

Fast with the late-breaking news, Gizmodo is reporting that Apple is now scanning submissions for illegal use of private APIs.. Really? Oh, no!

Only, this, um… isn't exactly new. This was already happening, and was already pretty widely known about three weeks ago.



Thursday, November 19, 2009

Update to the MPMediaItemCollection Category

While testing and debugging, I made a few tweaks to this category. I still haven't added reordering, but I think the methods that are here are now pretty solid. If you downloaded the earlier one, you probably want to grab the updated version, as it now codes defensively so problems aren't caused when a collection becomes empty by deletion (media item collections can't be created without at least one media item).

MPMediaItemCollection-Utils.h
#import <Foundation/Foundation.h>
#import <MediaPlayer/MediaPlayer.h>

@interface MPMediaItemCollection(Utils)
/** Returns the first media item in the collection
*/

- (MPMediaItem *)firstMediaItem;

/** Returns the last media item in the collection
*/

- (MPMediaItem *)lastMediaItem;

/** This method will return the item in this media collection at a specific index
*/

- (MPMediaItem *)mediaItemAtIndex:(NSUInteger)index;

/** Given a particular media item, this method will return the next media item in the collection.
If there are multiple copies of the same media item in the list, it will return the one
after the first occurrence.
*/

- (MPMediaItem *)mediaItemAfterItem:(MPMediaItem *)compare;

/** Returns the title of the media item at a given index.
*/

- (NSString *)titleForMediaItemAtIndex:(NSUInteger)index;

/** Returns YES if the given media item occurs at least once in this collection
*/

- (BOOL)containsItem:(MPMediaItem *)compare;

/** Creates a new collection by appending otherCollection to the end of this collection
*/

- (MPMediaItemCollection *)collectionByAppendingCollection:(MPMediaItemCollection *)otherCollection;

/** Creates a new collection by appending an array of media items to the end of this collection
*/

- (MPMediaItemCollection *)collectionByAppendingMediaItems:(NSArray *)items;

/** Creates a new collection by appending a single media item to the end of this collection
*/

- (MPMediaItemCollection *)collectionByAppendingMediaItem:(MPMediaItem *)item;

/** Creates a new collection based on this collection, but excluding the specified items.
*/

- (MPMediaItemCollection *)collectionByDeletingMediaItems:(NSArray *)itemsToRemove;

/** Creates a new collection based on this collection, but which doesn't include the specified media item.
*/

- (MPMediaItemCollection *)collectionByDeletingMediaItem:(MPMediaItem *)itemToRemove;

/** Creates a new collection based on this collection, but excluding the media item at the specified index
*/

- (MPMediaItemCollection *)collectionByDeletingMediaItemAtIndex:(NSUInteger)index;

/** Creates a new collection, based on this collection, but excluding the media items starting with
(and including) the objects at index from and ending with (and including) to.
*/

- (MPMediaItemCollection *)collectionByDeletingMediaItemsFromIndex:(NSUInteger)from toIndex:(NSUInteger)to;
@end



MPMediaItemCollection-Utils.m
#import "MPMediaItemCollection-Utils.h"

@implementation MPMediaItemCollection(Utils)
- (MPMediaItem *)firstMediaItem {
return [[self items] objectAtIndex:0];
}


- (MPMediaItem *)lastMediaItem {
return [[self items] lastObject];
}


- (MPMediaItem *)mediaItemAtIndex:(NSUInteger)index {
return [[self items] objectAtIndex:index];
}


- (MPMediaItem *)mediaItemAfterItem:(MPMediaItem *)compare {
NSArray *items = [self items];

for (MPMediaItem *oneItem in items) {
if ([oneItem isEqual:compare]) {
// If last item, there is no index + 1
if (![[items lastObject] isEqual: oneItem])
return [items objectAtIndex:[items indexOfObject:oneItem] + 1];
}

}

return nil;
}


- (NSString *)titleForMediaItemAtIndex:(NSUInteger)index {
MPMediaItem *item = [[self items] objectAtIndex:index];
return [item valueForProperty:MPMediaItemPropertyTitle];
}


- (BOOL)containsItem:(MPMediaItem *)compare {
NSArray *items = [self items];

for (MPMediaItem *oneItem in items) {
if ([oneItem isEqual:compare])
return YES;
}

return NO;
}


- (MPMediaItemCollection *)collectionByAppendingCollection:(MPMediaItemCollection *)otherCollection {
return [self collectionByAppendingMediaItems:[otherCollection items]];
}


- (MPMediaItemCollection *)collectionByAppendingMediaItems:(NSArray *)items {
if (items == nil || [items count] == 0)
return nil;
NSMutableArray *appendCollection = [[[self items] mutableCopy] autorelease];
[appendCollection addObjectsFromArray:items];
return [MPMediaItemCollection collectionWithItems:appendCollection];
}


- (MPMediaItemCollection *)collectionByAppendingMediaItem:(MPMediaItem *)item {
if (item == nil)
return nil;

return [self collectionByAppendingMediaItems:[NSArray arrayWithObject:item]];
}


- (MPMediaItemCollection *)collectionByDeletingMediaItems:(NSArray *)itemsToRemove {
if (itemsToRemove == nil || [itemsToRemove count] == 0)
return [[self copy] autorelease];
NSMutableArray *items = [[[self items] mutableCopy] autorelease];
[items removeObjectsInArray:itemsToRemove];
return [MPMediaItemCollection collectionWithItems:items];
}


- (MPMediaItemCollection *)collectionByDeletingMediaItem:(MPMediaItem *)itemToRemove {
if (itemToRemove == nil)
return [[self copy] autorelease];

NSMutableArray *items = [[[self items] mutableCopy] autorelease];
[items removeObject:itemToRemove];
return [MPMediaItemCollection collectionWithItems:items];
}


- (MPMediaItemCollection *)collectionByDeletingMediaItemAtIndex:(NSUInteger)index {
NSMutableArray *items = [[[self items] mutableCopy] autorelease];
[items removeObjectAtIndex:index];
return [items count] > 0 ? [MPMediaItemCollection collectionWithItems:items] : nil;
}


- (MPMediaItemCollection *)collectionByDeletingMediaItemsFromIndex:(NSUInteger)from toIndex:(NSUInteger)to {

// Ensure from is before to
if (to < from) {
NSUInteger temp = from;
to = from;
from = temp;
}


NSMutableArray *items = [[[self items] mutableCopy] autorelease];
[items removeObjectsInRange:NSMakeRange(from, to - from)];
return [MPMediaItemCollection collectionWithItems:items];
}

@end