19

是否可以观察(订阅)存储在 NSMutableDictionary 中不同键下的值的更改?在我的情况下,启动订阅时密钥已经存在,但是值发生了变化,我希望在这种情况下得到通知。我想要通知中更改值的键。

我假设如果我的字典键都是 NSString 实例,我可以单独订阅每个键路径。但是如果我的键不是字符串呢?在那种情况下我不走运吗?

4

3 回答 3

7

这是一个有趣的想法。我在 NSDictionary 或 NSDictionaryController 中找不到任何看起来很有希望的东西。我的第一直觉是在 NSMutableDictionary 周围使用组合并拦截对 setObject:forKey: 的调用(可能还有 -removeObjectForKey:) 以通知订阅者更改。

如果您选择走那条路线,有一个关于子类化 NSMutableDictionary 的Cocoa With Love 帖子可能会很有用。我还创建了自己的 NSMutableDictionary 子类,欢迎您使用开源代码。

您可以设计一个观察者协议,该协议可以指定应监视的特定键。代码不应该太多,但比我现在有时间扔掉的要多。

于 2009-07-09T23:13:38.600 回答
6

我现在已经成功地实现了这一点,使用 NSMutableDictionary 周围的组合。我很惊讶它花了这么少的代码。我实现的类是 Board,用来表示棋盘游戏中的模型。任何人都可以通过调用 addObserver: 方法订阅板状态的更改,实现如下:

- (void)addObserver:(id)observer {
    for (id key in grid)
        [self addObserver:observer
               forKeyPath:[key description]
                  options:0
                  context:key];
}

由于只能使用 KVO 模型订阅密钥,所以我欺骗并订阅了密钥的描述,但将密钥本身作为上下文传递。在观察我的 Board 实例的对象中,我实现了-observeValueForKeyPath:ofObject:change:context:忽略 keyPath 字符串并只使用传入的上下文。

对于我使用此方法创建的人工属性,我的简单 Board 类不符合 KVO,因此我传入0了该options属性,因此 KVO 机器不会尝试获取这些键的旧/新值。那会导致我的代码爆炸。

任何改变板子的事情(在我的简单类中只有一种方法可以做到这一点)引发必要的通知以使 KVO 机器开始行动:

- (void)setPiece:(id)piece atLocation:(Location*)loc {
    [self willChangeValueForKey:[loc description]];
    [grid setObject:piece forKey:loc];
    [self didChangeValueForKey:[loc description]];
}

瞧!使用非字符串键订阅 NSMutableDictionary!

于 2009-07-10T19:49:43.097 回答
0

尽管我出于不同的原因并以不同的方式使用 NSMutableDictionary(我没有将其子类化),但我找到了另一种方法。

我正在使用 NSMutableDictionary,因为它很好地从/到 JSON 序列化和反序列化。要获取和设置值,我使用“包装器”。这些对象使用字典作为“原始”实体并提供获取器和设置器来访问值。然后,我的 getter 和 setter 实现只需定义键(和对象类型)。

还有一个基类提供我将这些字典传递给(或从中获取)的属性。

@protocol PMBase <NSObject>
@property (nonatomic, strong, nullable) NSMutableDictionary<NSString*, id>* entity;
@end

@interface MBase : NSObject<PMBase>
@property (nonatomic, strong, nullable) NSMutableDictionary<NSString*, id>* entity;
@end

inline static NSString* _Nullable mbase_get_string(id<PMBase> _Nonnull const obj,
                                                   NSString* _Nonnull const key,
                                                   NSString* _Nullable const fallback) {
    id const val = [obj.entity objectForKey:key];
    return [val isKindOfClass:NSString.class] ? val : fallback;
}

inline static void mbase_set_string(id<PMBase> _Nonnull const obj,
                                    NSString* _Nonnull const key,
                                    NSString* _Nullable const value) {
    if (value) {
        [obj.entity setObject:value forKey:key];
    } else {
        [obj.entity removeObjectForKey:key];
    }
}

inline static NSNumber* _Nullable mbase_get_number(id<PMBase> _Nonnull const obj,
                                                   NSString* _Nonnull const key,
                                                   NSNumber* _Nullable const fallback) {
    id const val = [obj.entity objectForKey:key];
    return [val isKindOfClass:NSNumber.class] ? val : fallback;
}

inline static void mbase_set_number(id<PMBase> _Nonnull const obj,
                                    NSString* _Nonnull const key,
                                    NSNumber* _Nullable const value) {
    if (value) {
        [obj.entity setObject:value forKey:key];
    } else {
        [obj.entity removeObjectForKey:key];
    }
}

(MBase implementation is no magic, so no code here.)

在子类中,我只定义了额外的属性,在这里我覆盖了 getter 和 setter。子类几乎实现了访问器,有时它们返回其他MBase子类的实例。

@protocol PMDevice <NSObject>
// Not important here
@end

@interface MDevice : MBase <PMDevice>

@property (nonatomic, strong, nonnull) NSString* deviceName;
@property (nonatomic, assign) MDeviceType deviceType;             // phone, tablet... 

@end

@implementation MDevice

- (void)setDeviceName:(NSString*)name {
    mbase_set_string(self, @"name", name);
}

- (NSString*)deviceName {
    return mbase_get_string(self, @"name", NSLocalizedString(@"Unnamed", @"unnamed device placeholder"));
}

- (void)setDeviceType:(MDeviceType)deviceType {
    mbase_set_number(self, @"type", [NSNumber numberWithInt:(int)deviceType]);
}

- (MDeviceType)deviceType {
    return (MDeviceType)mbase_get_number(self, @"type", [NSNumber numberWithInt:MDeviceTypeOther]).intValue;
}

@end    

现在我需要密钥,包装器用来访问特定的字典值。我只是将“最后使用的密钥”作为属性添加到我的 MBase 中,并将该行添加到 getter 和 setter 内联:

@protocol PMBase <NSObject>
@property (nonatomic, strong, nullable) NSString* lastUsedKey;
@property (nonatomic, strong, nullable) NSMutableDictionary<NSString*, id>* entity;
@end

@interface MBase : NSObject<PMBase>
@property (nonatomic, strong, nullable) NSString* lastUsedKey;
@property (nonatomic, strong, nullable) NSMutableDictionary<NSString*, id>* entity;
@end

inline static NSString* _Nullable mbase_get_string(id<PMBase> _Nonnull const obj,
                                                   NSString* _Nonnull const key,
                                                   NSString* _Nullable const fallback) {
    obj.lastUsedKey = key;
    id const val = [obj.entity objectForKey:key];
    return [val isKindOfClass:NSString.class] ? val : fallback;
}

inline static void mbase_set_string(id<PMBase> _Nonnull const obj,
                                    NSString* _Nonnull const key,
                                    NSString* _Nullable const value) {
    obj.lastUsedKey = key;
    if (value) {
        [obj.entity setObject:value forKey:key];
    } else {
        [obj.entity removeObjectForKey:key];
    }
}

inline static NSNumber* _Nullable mbase_get_number(id<PMBase> _Nonnull const obj,
                                                   NSString* _Nonnull const key,
                                                   NSNumber* _Nullable const fallback) {
    obj.lastUsedKey = key;
    id const val = [obj.entity objectForKey:key];
    return [val isKindOfClass:NSNumber.class] ? val : fallback;
}

inline static void mbase_set_number(id<PMBase> _Nonnull const obj,
                                    NSString* _Nonnull const key,
                                    NSNumber* _Nullable const value) {
    obj.lastUsedKey = key;
    if (value) {
        [obj.entity setObject:value forKey:key];
    } else {
        [obj.entity removeObjectForKey:key];
    }
}

现在,每当我需要有关密钥的信息时(如果我遇到这样做的原因,密钥可能会发生变化),我将访问一个值获取器,然后访问该lastUsedKey属性。

于 2020-06-04T07:05:11.600 回答