How to observe a collection of objects and changes to their properties

The easy way is to use an NSArrayController and add an observer like so:

[friendsArrayController addObserver:self forKeyPath:@"arrangedObjects.name" options:0 context:nil];

The hard way is to observe a model’s collection property and add/remove observers when objects get added/removed from the collection like so:

[person addObserver:self forKeyPath:@"friends" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if (object == person && [keyPath isEqualTo:@"friends"]) {
            if ([[change valueForKey:NSKeyValueChangeKindKey] intValue] == NSKeyValueChangeInsertion) {
                for (Friend *newFriend in [change valueForKey:NSKeyValueChangeNewKey]) {
                    if (![observedFriends containsObject:newFriend]) {
                        [newFriend addObserver:self forKeyPath:@"name" options:0 context:nil];
                    }
                }
            }
            else if ([[change valueForKey:NSKeyValueChangeKindKey] intValue] == NSKeyValueChangeRemoval) {
                for (Friend *oldFriend in [change valueForKey:NSKeyValueChangeOldKey]) {
                    if (![observedFriends containsObject:oldFriend]) {
                        [oldFriend removeObserver:self forKeyPath:@"name"];
                    }
                }
            }
            else if ([[change valueForKey:NSKeyValueChangeKindKey] intValue] == NSKeyValueChangeReplacement) {
                for (Friend *newFriend in [change valueForKey:NSKeyValueChangeNewKey]) {
                    if (![observedFriends containsObject:newFriend]) {
                        [newFriend addObserver:self forKeyPath:@"name" options:0 context:nil];
                    }
                }
                for (Friend *oldFriend in [change valueForKey:NSKeyValueChangeOldKey]) {
                    if (![observedFriends containsObject:oldFriend]) {
                        [oldFriend removeObserver:self forKeyPath:@"name"];
                    }
                }
            }
    }
}
Advertisements

Using NSTreeController with models that contain different children keypaths

If you use NSTreeController, you usually specify the models’ children keypath through Interface Builder, but what do you do if your models have different keypaths?

The first thing you probably did was define a children alias @property on the managed models. If you tried this, you’d see that the tree will at first draw correctly, but doesn’t refresh when your models update. A quick fix would’ve been to create controllers and observers to manually call [treeController rearrangeobjects]. This isn’t necessary. If setup properly, your NSTreeController can automatically update itself through KVO.

Looking at the NSTreeController reference we see this, “If necessary, you should implement accessor methods in your model classes, that map the child key name to the appropriate class-specific method name.”

How do we do this?

NSKeyValueObserving protocol has a method named +keyPathsForValuesAffecting that we can use to synchronize the the real and alias properties.


Your models end up looking something like:

@interface SomeModel: NSManagedObject
@property (nonatomic, strong)   NSSet *realChildren; // real
@property (nonatomic, readonly) NSSet *children;     // alias
@end

@implementation SomeModel
@dynamic someChildren;                              // real
- (NSSet *)children { return self.realChildren; }   // alias
+ (NSSet *)keyPathsForValuesAffectingChildren {     // synchronize real and alias
    return [NSSet setWithObject:@"realChildren"];
}
@end