Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

KVO infinite recurrence / loop

I want to be able to change a (calendar) event, which has a startDate and an endDate. There's also a duration.

  1. When the user changes the endDate, the duration is updated.
  2. When the user changes the startDate, the endDate changed according to the duration.
  3. When the user changes the duration, the endDate changes.

This last action would trigger the first action, which would trigger the third, which would trigger the first, ad infinitum (or when the stack fills up).

Lines like the following, to change the values, cause this loop:

        [self setValue:[NSNumber numberWithLong:(interval%60)] forKeyPath:@"durationMinutes"];
        [self setValue:ed forKeyPath:@"endDate"];

To simply stop observing, and restart after a change, is not attractive since the values in the GUI won't get updated. The question, then, is: How can I safely (and elegantly) update one of two interdependent properties?

like image 503
Carelinkz Avatar asked Jan 09 '13 21:01

Carelinkz


3 Answers

You can bypass KVO notifications when necessary by using setPrimitiveValue:forKey:. That sets the value but does not trigger any notifications. It also bypasses any custom setter you might have for the property. That should break the call cycle.

When using this method you generally want to call the "will change" and "did change" methods to ensure that the Core Data state is maintained. That is, something like:

[self willChangeValueForKey:@"endDate"];
[self setPrimitiveValue:ed forKey:@"endDate"];
[self didChangeValueForKey:@"endDate"];

This avoids the need for a flag-- you just say, hey, set the value and no messing around, OK?

like image 80
Tom Harrington Avatar answered Nov 02 '22 23:11

Tom Harrington


The best way would be to write custom setters so that they update the values only if they have changed:

For instance:

- (void)setDurationMinutes:(NSNumber *)minutes
{
    if (![minutes isEqual:self.durationMinutes])
    {
        _durationMinutes = minutes;
        self.endDate = [self.startDate dateByAddingTimeInterval:minutes * 60]; 
    }
}

- (void)setEndDate:(NSDate *)date
{
    if (![_endDate isEqualToDate:date])
    {
        _endDate = date;
        self.durationMinutes = [date timeIntervalSinceDate:self.startDate] / 60;
    }
}

So now, when you set the minutes to a different value, it will update and set the end date. End date will be different so it will update and set the duration. However, this time the duration will be the same (since it was previously set) and it won't attempt to set itself or the end date again.

It also keeps your code nice and clean, and clear about what changes what and when.

like image 37
lnafziger Avatar answered Nov 02 '22 23:11

lnafziger


Combine all three into one method.

- (void)updateEndDate:(NSDate *)end
            startDate:(NSDate *)start
             duration:(NSNumber *)duration
{
  ... insert your update logic here
  [self willChangeValueForKey:@"startDate"];
  [self willChangeValueForKey:@"endDate"];
  [self willChangeValueForKey:@"duration"];
  _endDate = end;
  _startDate = start;
  _duration = duration;
  [self didChangeValueForKey:@"duration"];
  [self didChangeValueForKey:@"endDate"];
  [self didChangeValueForKey:@"startDate"];
}

Call that method in your settors

-(void)setStartDate:(NSDate *)start
{
   [self updateEndDate:self.endDate startDate:start duration:self.duration];
}

KVO compliance requires you to specify that you are manually sending notifications for the properties

+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey
{

    BOOL automatic = NO;
    NSArray * manualKeys = @[@"endDate", @"startDate", @"duration"];
    if ([manualKeys containsObject:theKey])
    {
        automatic = NO;
    }
    else
    {
        automatic = [super automaticallyNotifiesObserversForKey:theKey];
    }
    return automatic;
}
like image 36
Fruity Geek Avatar answered Nov 03 '22 00:11

Fruity Geek