Search code examples
objective-ckey-value-observing

Mutate observed property in KVO observeValueForKeyPath


I have a view controller with a NSMutableArray property observed by KVO :

@interface MyViewController ()

@property (strong, nonatomic) NSMutableArray *values;

@end

@implementation MyViewController

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.values = [NSMutableArray array];

    [self addObserver:self forKeyPath:@"self.values" options:0 context:nil];
}

- (void)dealloc
{
    [self removeObserver:self forKeyPath:@"self.values"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if ([keyPath isEqualToString:@"self.values"])
    {
        // The code here is a bit more complex than that, but the main idea
        // is to mutate the object
        [self.values addObject:[NSDate date]]; // This line triggers a KVO notification, causing an infinite recursion 
    }
}

@end

The goal of my implementation is to apply discounts on each product in the array, and recalculate theses discouts each time a product is added to the array.

I'd like to modify the observed property when a notification is received, but obviously the code above do not work, it goes into infinite recursion.

One solution would be to remove the observer at the beginning of observeValueForKeyPath... and re-add the observer at the end.

This seems a bit ugly though, and may cause problems is the property is modified on one thread while observeValueForKeyPath... is running on another.

EDIT: As Wain pointed out, the real problem here is that my NSMutableArray self.value could contain immutable object (NSDictionnarys) and I have no way to replace them by their mutable equivalent with something like:

[self.value replaceObjectAtIndex:0 withObject:[self.value[0] mutableCopy]];

without triggering the KVO notification.


Solution

  • If you value your time, don't add or remove KVO observations during calls to observeValueForKeyPath:.... For details, go read my answer to this other question.

    If you really need to do this, you can add an ivar that serves as a flag to stop the recursion. It might look something like this:

    @interface MyViewController ()
    
    @property (strong, nonatomic) NSMutableArray *values;
    
    @end
    
    @implementation MyViewController
    {
        BOOL _updatingValuesInObservation;
    }
    
    static void * const MyValueObservation = (void*)&MyValueObservation;
    
    - (void)viewDidLoad
    {
        [super viewDidLoad];
    
        self.values = [NSMutableArray array];
    
        [self addObserver:self forKeyPath:@"values" options:0 context:MyValueObservation];
    }
    
    - (void)dealloc
    {
        [self removeObserver:self forKeyPath:@"values" context:MyValueObservation];
    }
    
    - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
    {
        if (MyValueObservation == context && !_updatingValuesInObservation)
        {
            _updatingValuesInObservation = YES;
            [self.values addObject:[NSDate date]];
            _updatingValuesInObservation = NO; 
        }
        else
        {
            [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        }
    }
    
    @end
    

    Also note the use of the context parameter and the calling of super, both good habits to be in for more bullet-proof code.

    Lastly, you're not doing this here, but it seems like you're on the path, so I'll mention it: Remember that observing a collection is not the same as observing all the items in a collection.