Key-Value Observing With Blocks
Saturday, March 23, 13
What is key-value observing? • Allows you to be notified whenever a
particular property of an object changes
• Used heavily by bindings on OS X • Can also be used to manually observe objects
Saturday, March 23, 13
KVO API •
- (void)addObserver:(id)observer forKeyPath:(NSString*)keyPath options:(NSKeyValueObservingOptions)options context:(void*)context;
•
- (void)removeObserver:(id)observer forKeyPath: (NSString*)keyPath context:(void*)context;
•
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context;
Saturday, March 23, 13
KVO vs. NSNotificationCenter • No “KVONotificationCenter” object • No callback selector, all notifications come through -observeValueForKeyPath:...
• Must observe each object individually, i.e. no equivalent of observing nil
Saturday, March 23, 13
static NSString* MyObservationContext = @”MyObservationContext”; - (void)loadView { [super loadView]; [[self representedObject] addObserver:self forKeyPath:@”name” options:0 context:MyObservationContext]; [[self representedObject] addObserver:self forKeyPath:@”icon” options:0 context:MyObservationContext]; } - (void)observeValueForKeyPath:(NSString *)aKeyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (object == [self representedObject] && context == MyObservationContext) { if ([keyPath isEqualToString:@”name”]) [self updateNameTextField]; else if ([keyPath isEqualToString:@”icon”]) [self updateIcon]; } else [super observeValueForKeyPath:aKeyPath ofObject:object change:change context:context]; }
Saturday, March 23, 13
KVO downsides • -observeValueForKeyPath:... gets unruly fast if you’re making multiple observations
• Need to make sure that any observations
that you didn’t register get passed to super
Saturday, March 23, 13
KVO with blocks • •
A more straightforward way of managing observations Simply pass a block that you want to be called when the value changes - (void)loadView { [[self representedObject] addObserverForKeyPath:@”name” task:^(id object, NSDictionary* change) { [self updateNameTextField]; }]; [[self representedObject] addObserverForKeyPath:@”icon” task:^(id object, NSDictionary* change) { [self updateIcon]; }]; }
Saturday, March 23, 13
KVONotificationManager https://github.com/schwa/KVONotification-Manager • •
Simple implementation, adds a category to NSObject Uses an identifier object to differentiate each observation for purposes of removal typedef void (^KVOBlock)(NSString *keyPath, id object, NSDictionary *change, id identifier); - (void)addObserver:(NSObject *)observer handler:(KVOBlock)inHandler forKeyPath:(NSString *)inKeyPath options:(NSKeyValueObservingOptions)inOptions identifier:(id)inIdentifier; - (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)inKeyPath identifier:(id)inIdentifier;
Saturday, March 23, 13
KVO+Blocks https://gist.github.com/153676 • More complex implementation • Uses objc_setAssociatedObject() to store observations for each observed object
•
Allows notifications to be posted to an NSOperationQueue
• •
Adds additional hooks to ensure thread safety
Saturday, March 23, 13
Returns a token object that you must hang on to if you want to remove the observation
AMObserver API typedef void (^AMBlockTask)(id obj, NSDictionary *change); - (AMBlockToken *)addObserverForKeyPath: (NSString *)keyPath onQueue:(NSOperationQueue *)queue task:(AMBlockTask)task; - (void)removeObserverWithBlockToken: (AMBlockToken *)token;
Saturday, March 23, 13
Implementation •
Both use an internal helper class to manage the actual observation (AMObserverTrampoline, CKVOBlockNotificationHelper)
•
One object is created for each time an observer is added
•
The helper implements observeValueForKeyPath:... and simply calls through to the callback block
Saturday, March 23, 13
My mashup • Based mostly on KVO+Blocks • Uses passed-in identifier ala
KVONotificationManager instead of returned token (easier for removal, IMO)
• Adds new parameter for KVO observing options
Saturday, March 23, 13
My API - (void)addObserverForKeyPath:(NSString*)inKeyPath withOptions:(NSKeyValueObservingOptions)inOptions identifier:(NSString*)inIdentifier onQueue:(NSOperationQueue*)inQueue task:(void (^)(id obj, NSDictionary *change))task; - (void)addObserverForKeyPath:(NSString *)keyPath task: (void (^)(id obj, NSDictionary *change))task; //simpler cover method - (void)removeObserverForKeyPath:(NSString*)inKeyPath identifier:(NSString*)inIdentifier;
Saturday, March 23, 13
Retain cycles •
Retain cycles formed when referencing self in your callback block
•
Approach 1: Use __weak attribute __weak id blockSelf = self; [representedObject addObserverForKeyPath:@”name” task:^(id object, NSDictionary* change) { [self updateNameTextField]; //NO: forms retain cycle [blockSelf updateNameTextField]; //YES: __weak vars are not retained }];
Saturday, March 23, 13
Problems with __weak •
Can lead to unexpected (though correct) behavior if the object gets deallocated before the block is called
•
Reference inside the block will be nil - might work OK, might not
•
Things gets more hairy if the object is deallocated while the block is executing
•
Reference will be valid initially, then all of a sudden become nil
Saturday, March 23, 13
__weak and __strong __weak id weakSelf = self; [representedObject addObserverForKeyPath:@”name” task:^(id object, NSDictionary* change) { __strong id strongSelf = weakSelf; [strongSelf updateNameTextField]; }];
•
Saturday, March 23, 13
This way, strongSelf will either be nil or a valid reference through the entire block
Removing observers •
Like NSNotifications, you must remove your observation when you’re deallocated - (void)dealloc { [[self representedObject] removeObserverForKeyPath:@”name”] identifier:kObservationIdentifier]; [super dealloc]; }
Saturday, March 23, 13
Things to watch out for • Because observations are self contained, it can be easy to lose track of where you make them in your code
• I typically have a -setupObservations
method for most classes, and make all observations there
Saturday, March 23, 13
Da Code
• My version of KVO+Blocks is available on Github at https://gist.github.com/1217658
Saturday, March 23, 13
Summary • Key-value observing is very useful, but the default API is somewhat lacking
• Using blocks for callbacks improves code readability, scalability
Saturday, March 23, 13