KVO support when using composition for NSMutableDictionary extension

I have an array of NSMutableDictionary objects that are displayed in the master-detail interface, which has several text fields and a bunch of flags. The controls are tied to the dictionary keys, which are accessed through the selection array controller.

I would like to add some logic that clears one checkbox when another is cleared, and restores the original value if it is re-checked in the same session. Since I need to associate storage with a dictionary, and I also need to add code, I thought I would use composition for the NSMutableDictionary extension.

Here is what I did:

  • I created a subclass of LibraryEntry that contains NSMutableDictionary.
  • I implemented forwardInvocation: respondsToSelector: methodSignatureForSelector: and after some trial error valueForUndefinedKey:
  • I created forwarder objects.
  • I left the bindings the way they were.

It loads the data just fine, but I assume that KVO will not work correctly. I assume that the binder calls addObserver: on my object, but I have not implemented anything special to handle it.

I thought to just override addObserver: and forward the message to the dictionary. But if I do this, observeValueForKey: notifications will not come from my object (addObserver source receiver), but from the dictionary.

Before I tried to implement more transparent forwarding for these KVO calls, I thought ... it gets messy. I continue to read "using composition, not subclasses" to get this behavior. Is this just the wrong template for this situation? What for? Due to KVO?

It seems that I would have cleaner results if I abandon the composition and choose one of these alternatives:

  • Use decorators with one instance watching each dictionary
  • Save the transition keys in the dictionary and ask the controller to delete them before saving
  • Contribute to the dictionary and declare properties

Here is my code, if it is useful ( values - dictionary):

 - (void)forwardInvocation:(NSInvocation *)anInvocation { if ([values respondsToSelector:[anInvocation selector]]) [anInvocation invokeWithTarget:values]; else [super forwardInvocation:anInvocation]; } - (BOOL)respondsToSelector:(SEL)aSelector { if ( [super respondsToSelector:aSelector] ) return YES; else return [values respondsToSelector:aSelector]; } - (NSMethodSignature*)methodSignatureForSelector:(SEL)selector { NSMethodSignature* signature = [super methodSignatureForSelector:selector]; if (!signature) signature = [values methodSignatureForSelector:selector]; return signature; } -(id)valueForUndefinedKey:(NSString *)key { return [values valueForKey:key]; } 
+4
source share
1 answer

I think that with a combination of Objective-C linked storage and some blocks, you can connect arbitrary behavior to a dictionary (or any other object compatible with KVO) and solve this problem this way. I have prepared the following idea, which implements the general mechanism of KVO-trigger-blocks and an encoded example and an example that seems to fulfill what you want and does not require subclassing or decorating the foundation collections.

First, the open interface of this mechanism:

 typedef void (^KBBehavior)(id object, NSString* keyPath, id oldValue, id newValue, id userInfo); @interface NSObject (KBKVOBehaviorObserver) - (void)addBehavior: (KBBehavior)block forKeyPath: (NSString*)keyPath options: (NSKeyValueObservingOptions)options userInfo: (id)userInfo; - (void)removeBehaviorForKeyPath: (NSString*)keyPath; @end 

This will allow you to bind block-based observations / behaviors to arbitrary objects. The task that you describe using checkboxes might look something like this:

 - (void)testBehaviors { NSMutableDictionary* myModelDictionary = [NSMutableDictionary dictionary]; KBBehavior behaviorBlock = ^(id object, NSString* keyPath, id oldValue, id newValue, id userInfo) { NSMutableDictionary* modelDictionary = (NSMutableDictionary*)object; NSMutableDictionary* previousValues = (NSMutableDictionary*)userInfo; if (nil == newValue || (![newValue boolValue])) { // If the master is turning off, turn off the slave, but make a note of the previous value id previousValue = [modelDictionary objectForKey: @"slaveCheckbox"]; if (previousValue) [previousValues setObject: previousValue forKey: @"slaveCheckbox"]; else [previousValues removeObjectForKey: @"slaveCheckbox"]; [modelDictionary setObject: newValue forKey: @"slaveCheckbox"]; } else { // if the master is turning ON, restore the previous value of the slave id prevValue = [previousValues objectForKey: @"slaveCheckbox"]; if (prevValue) [modelDictionary setObject:prevValue forKey: @"slaveCheckbox"]; else [modelDictionary removeObjectForKey: @"slaveCheckbox"]; } }; // Set the state... [myModelDictionary setObject: [NSNumber numberWithBool: YES] forKey: @"slaveCheckbox"]; [myModelDictionary setObject: [NSNumber numberWithBool: YES] forKey: @"masterCheckbox"]; // Add behavior [myModelDictionary addBehavior: behaviorBlock forKeyPath: @"masterCheckbox" options: NSKeyValueObservingOptionNew userInfo: [NSMutableDictionary dictionary]]; // turn off the master [myModelDictionary setObject: [NSNumber numberWithBool: NO] forKey: @"masterCheckbox"]; // we now expect the slave to be off... NSLog(@"slaveCheckbox value: %@", [myModelDictionary objectForKey: @"slaveCheckbox"]); // turn the master back on... [myModelDictionary setObject: [NSNumber numberWithBool: YES] forKey: @"masterCheckbox"]; // now we expect the slave to be back on, since that was it previous value NSLog(@"slaveCheckbox value: %@", [myModelDictionary objectForKey: @"slaveCheckbox"]); } 

I implemented block / KVO binding by creating an object to track blocks and userInfos, and then it will be a KVO observer. Here is what I did:

 #import <objc/runtime.h> static void* kKVOBehaviorsKey = &kKVOBehaviorsKey; @interface KBKVOBehaviorObserver : NSObject { NSMutableDictionary* mBehaviorsByKey; NSMutableDictionary* mUserInfosByKey; } @end @implementation KBKVOBehaviorObserver - (id)init { if (self = [super init]) { mBehaviorsByKey = [[NSMutableDictionary alloc] init]; mUserInfosByKey = [[NSMutableDictionary alloc] init]; } return self; } - (void)dealloc { [mBehaviorsByKey release]; mBehaviorsByKey = nil; [mUserInfosByKey release]; mUserInfosByKey = nil; [super dealloc]; } - (void)addBehavior: (KBBehavior)block forKeyPath: (NSString*)keyPath userInfo: (id)userInfo { @synchronized(self) { id copiedBlock = [[block copy] autorelease]; [mBehaviorsByKey setObject: copiedBlock forKey: keyPath]; [mUserInfosByKey setObject: userInfo forKey: keyPath]; } } - (void)removeBehaviorForKeyPath: (NSString*)keyPath { @synchronized(self) { [mUserInfosByKey removeObjectForKey: keyPath]; [mBehaviorsByKey removeObjectForKey: keyPath]; } } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == kKVOBehaviorsKey) { KBBehavior behavior = nil; id userInfo = nil; @synchronized(self) { behavior = [[[mBehaviorsByKey objectForKey: keyPath] retain] autorelease]; userInfo = [[[mUserInfosByKey objectForKey: keyPath] retain] autorelease]; } if (behavior) { id oldValue = [change objectForKey: NSKeyValueChangeOldKey]; id newValue = [change objectForKey: NSKeyValueChangeNewKey]; behavior(object, keyPath, oldValue, newValue, userInfo); } } } @end @implementation NSObject (KBKVOBehaviorObserver) - (void)addBehavior: (KBBehavior)block forKeyPath: (NSString*)keyPath options: (NSKeyValueObservingOptions)options userInfo: (id)userInfo { KBKVOBehaviorObserver* obs = nil; @synchronized(self) { obs = objc_getAssociatedObject(self, kKVOBehaviorsKey); if (nil == obs) { obs = [[[KBKVOBehaviorObserver alloc] init] autorelease]; objc_setAssociatedObject(self, kKVOBehaviorsKey, obs, OBJC_ASSOCIATION_RETAIN); } } // Put the behavior and userInfos into stuff... [obs addBehavior: block forKeyPath: keyPath userInfo: userInfo]; // add the observation [self addObserver: obs forKeyPath: keyPath options: options context: kKVOBehaviorsKey]; } - (void)removeBehaviorForKeyPath: (NSString*)keyPath { KBKVOBehaviorObserver* obs = nil; obs = [[objc_getAssociatedObject(self, kKVOBehaviorsKey) retain] autorelease]; // Remove the observation [self removeObserver: obs forKeyPath: keyPath context: kKVOBehaviorsKey]; // remove the behavior [obs removeBehaviorForKeyPath: keyPath]; } @end 

One thing that is regrettable is that you need to remove cases / behavior in order to break the transitive save cycle between the original dictionary and the object of observation, so if you do not delete the behavior, the collection leaks. But overall, this template should be useful.

Hope this helps.


+1
source

Source: https://habr.com/ru/post/1379550/


All Articles