BlackDog Foundry Bookmark This page

This post describes how to use KVC to bind the contents of a mutable array into an NSArrayController so that adding and removing elements from the underlying array will trigger corresponding behaviour in the array controller.

Basic Setup

For the purpose of this article, I’m going to use a straw-man class called BDHouse that has a property called rooms that will, surprisingly, contain BDRoom objects. For simplicity’s sake, each room will have a single property called name.

@interface BDHouse : NSObject
@property (nonatomic, strong) NSMutableArray *rooms;
@end
 
@interface BDRoom : NSObject
@property (nonatomic, strong) NSString *name;
@end

With these basic classes, you have enough to bind the contentArray of your NSArrayController to the rooms property of the house. The array controller can then be bound to an NSTableView or whatever UI control you are using and the UI will display the contents of your array. However, there are a few scenarios where the UI won’t display changes to your rooms.

What does work

Firstly, adding and removing rooms via the Array Controller‘s addObject: or removeObject: methods will work fine. The array controller will update the underlying model and also ensure the UI reflects the new state.

Secondly, if you replace the entire rooms value by doing something like:

NSMutableArray *rooms = [NSMutableArray array];
 
BDRoom *room1 = ...;
[rooms addObject:room1];
BDRoom *room2 = ...;
[rooms addObject:room2];
 
house.rooms = rooms;

What does NOT work

Adding/removing rooms by manipulating the mutable array directly. For example:

BDRoom *room = ...;
[house.rooms addObject:room];

This will add the room to the array, however, the array controller currently has no visibility of the changes. Fixing this problem is the point of this article.

KVC Compliance

You might be tempted to think that the array controller should be aware of the changes made to the rooms property. The reason it does not though is that the house object is not fully KVC-compliant for the rooms key.

But, I hear you exclaim, we’ve declared rooms as a property… surely it is already KVC compliant? The thing is that a property itself can’t be KVC compliant; rather, a class is (or is not) KVC-compliant for a particular property.

So, how do we make BDHouse KVC-compliant for the rooms key? Apple does a pretty good job of explaining it in their KVC Programming Guide, however I think, generally speaking, that the biggest problem here is that the linkage between NSArrayController and its expectation of KVC compliance is quite subtle and non-discoverable.

Adding KVC Compliance

Anyway, moving on… let’s change our class definition to add what we need:

@interface BDHouse : NSObject
@property (nonatomic, strong) NSMutableArray *rooms;
 
-(void)insertObject:(BDRoom *)room inRoomsAtIndex:(NSUInteger)index;
-(void)removeObjectFromRoomsAtIndex:(NSUInteger)index;
@end
 
 
 
@implementation BDHouse
-(instancetype)init {
	self = [super init];
	if (self != nil) {
		_rooms = [NSMutableArray array];
	}
	return self;
}
 
-(void)insertObject:(BDRoom *)room inRoomsAtIndex:(NSUInteger)index {
	self.rooms[index] = room;
}
-(void)removeObjectFromRoomsAtIndex:(NSUInteger)index {
	[self.rooms removeObjectAtIndex:index];
}
@end

Now, we can add rooms by using code like the following:

BDRoom *room = ...;
[house insertObject:room inRoomsAtIndex:[house.rooms count]];

and the array controller will happily pick up the changes and update the UI controls.

Tightening Up

While we have a working implementation now, we are still exposed by code that still uses the old way of adding rooms:

BDRoom *room = ...;
[house.rooms addObject:room];

as this bypasses the KVC machinery. Additionally, I don’t find the KVC-accessors to be a particularly friendly/readable API.

In order to enforce the rooms property is accessed in a KVC/KVO-compliant manner, we can make the following changes:

@interface BDHouse : NSObject
@property (nonatomic, strong) NSArray *rooms;
 
-(void)addRoom:(BDRoom *)room;
-(void)removeRoom:(BDRoom *)room;
 
@end
 
 
 
@implementation BDHouse {
	NSMutableArray *_rooms;
}
 
-(instancetype)init {
	self = [super init];
	if (self != nil) {
		_rooms = [NSMutableArray array];
	}
	return self;
}
 
// public API
-(void)addRoom:(BDRoom *)room {
	[self insertObject:room inRoomsAtIndex:[self.rooms count]];
}
-(void)removeRoom:(BDRoom *)room {
	[self removeObjectFromRoomsAtIndex:[self.rooms indexOfObject:room]];
}
 
// property accessors
-(NSArray *)rooms {
	return [_rooms copy];
}
-(void)setRooms:(NSArray *)rooms {
	[self willChangeValueForKey:@"rooms"];
	_rooms = [NSMutableArray arrayWithArray:rooms];
	[self didChangeValueForKey:@"rooms"];
}
 
// KVC compliant accessors
-(void)insertObject:(BDRoom *)room inRoomsAtIndex:(NSUInteger)index {
	_rooms[index] = room;
}
-(void)removeObjectFromRoomsAtIndex:(NSUInteger)index {
	[_rooms removeObjectAtIndex:index];
}
@end

Note that we are now backing our rooms property with a NSMutableArray and providing our own implementations of the getter/setter. So now, our BDHouse object is:

  • KVC-compliant for the rooms property
  • Offering a non-mutable rooms property so that consumers can access sensibly
  • Still offering a friendly API.

Yay!

Edited 24th May 2014: Fixed stupid typo in variable name

Categories

Copyright © 2012 BlackDog Foundry