Поддержка KVO при использовании композиции для расширения NSMutableDictionary - PullRequest
4 голосов
/ 04 ноября 2011

У меня есть массив объектов NSMutableDictionary, которые отображаются в интерфейсе master-detail, который имеет несколько текстовых полей и несколько флажков.Элементы управления привязаны к ключам словаря, доступ к которым осуществляется через контроллер массива selection.

. Я хотел бы добавить некоторую логику, которая очищает один флажок при сбросе другого и восстанавливает исходное значение, если оно перепроверенов том же сеансе.Поскольку мне нужно связать хранилище со словарем и добавить код, я решил использовать композицию для расширения NSMutableDictionary.

Вот что я сделал:

  1. Я создал LibraryEntry подкласс, который содержит NSMutableDictionary.
  2. Я реализовал forwardInvocation:, respondsToSelector:, methodSignatureForSelector: и после некоторых проб и ошибок valueForUndefinedKey:.
  3. Я создал свои объекты экспедитора.
  4. Я оставил привязки такими, как они были.

Он прекрасно загружает данные, но я предполагаю, что KVO не будет работать правильно.Я предполагаю, что механизм связывания вызывает addObserver: для моего объекта, но я не реализовал ничего особенного для его обработки.

Я думал просто переопределить addObserver: и переслать сообщение в словарь.Но если я сделаю так, что уведомления observeValueForKey: будут исходить не от моего объекта (первоначального получателя addObserver), а от словаря.

Прежде чем я попытался реализовать более прозрачную переадресацию для этих вызовов KVO, ядумал ... это становится грязнымЯ продолжаю читать «используйте композицию, а не подклассы», чтобы получить подобное поведение.Это просто неправильный шаблон для этой ситуации?Зачем?Из-за KVO?

Кажется, что я получил бы более чистые результаты, если бы я отказался от композиции и выбрал одну из этих альтернатив:

  1. Использовать декораторы, при этом один экземпляр наблюдал за каждым словарем
  2. Сохраните временные ключи в словаре и попросите контроллер удалить их перед сохранением.
  3. Избавьтесь от словаря и объявите свойства вместо этого

Вот мой код, на случай, если это будет полезно(values это словарь):

- (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];
}

1 Ответ

1 голос
/ 04 декабря 2011

Я думаю, что с помощью сочетания хранилища, связанного с Objective-C, и некоторых блоков, вы можете подключить произвольное поведение к словарю (или любому другому объекту, совместимому с KVO) и таким образом решить вашу проблему. Я разработал следующую идею, которая реализует общий механизм KVO-triggers-block, и кодировал ее, а также пример, который, кажется, делает то, что вы хотите, и не включает в себя создание подклассов или декорирование базовых коллекций.

Первый публичный интерфейс этого механизма:

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

Это позволит вам прикреплять наблюдения / поведение на основе блоков к произвольным объектам. Задача, которую вы описываете с помощью флажков, может выглядеть примерно так:

- (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's previous value
    NSLog(@"slaveCheckbox value: %@", [myModelDictionary objectForKey: @"slaveCheckbox"]);


}

Я реализовал подключение блоков / KVO, создав объект для отслеживания блоков и userInfos, а затем стал наблюдателем KVO. Вот что я сделал:

#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

Одна вещь, которая вызывает сожаление, это то, что вам нужно удалить наблюдения / поведения, чтобы разорвать транзитивный цикл сохранения между исходным словарем и объектом наблюдения, поэтому, если вы не удалите поведения, вы ' пропущу коллекцию. Но в целом этот шаблон должен быть полезен.

Надеюсь, это поможет.


...