Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does NSOperation disable automatic key-value observing?

Tags:

When working with a custom NSOperation subclass I noticed that the automatic key-value observing is disabled by the [NSOperation automaticallyNotifiesObserversForKey] class method (which returns NO at least for some key paths). Because of that the code inside of NSOperation subclasses is littered by manual calls to willChangeValueForKey: and didChange…, as visible in many code samples on the web.

Why does NSOperation do that? With automatic KVO support people could simply declare properties for the operation lifecycle flags (isExecuting etc.) and trigger the KVO events through the accessors, ie. the following code:

[self willChangeValueForKey:@"isExecuting"];
executing = NO;
[self didChangeValueForKey:@"isExecuting"];
[self willChangeValueForKey:@"isFinished"];
finished = YES;
[self didChangeValueForKey:@"isFinished"];

…could be replaced by this:

[self setIsExecuting:NO];
[self setIsFinished:YES];

Is there a catch somewhere? I just overrode the automaticallyNotifiesObserversForKey to return YES and things seem to work fine.

like image 530
zoul Avatar asked Aug 26 '10 08:08

zoul


2 Answers

The most likely explanation is that the kvo keys don't match the standard conventions. Normally one has methods like -isExecuting and -setExecuting:, where the key path is @"executing". In the case of NSOperation, the key path is @"isExecuting" instead.

The other possibility is that most NSOperations don't actually have a method named -setIsExecuting: to change that value. Instead, they base the executing/finished flags on other internal state. In this case, one absolutely needs to use the explicit willChange/didChange notifications. For example, if I have an NSOperation that wraps an NSURLConnection, I may have 2 ivars, one named data that holds the downloaded data, and one named connection which holds the NSURLConnection, and I may implement the getters like so:

- (BOOL)isExecuting {
    return (connection != nil);
}

- (BOOL)isFinished {
    return (data != nil && connection == nil);
}

Now my -start method can use

[self willChangeValueForKey:@"isExecuting"];
data = [[NSMutableData alloc] init]; // doesn't affect executing, but is used later
connection = [[NSURLConnection connectionWithRequest:request delegate:self] retain];
[self didChangeValueForKey:@"isExecuting"];

to start executing, and

[self willChangeValueForKey:@"isExecuting"];
[self willChangeValueForKey:@"isFinished"];
[connection cancel];
[connection release];
connection = nil;
[self didChangeValueForKey:@"isFinished"];
[self didChangeValueForKey:@"isExecuting"];

to finish.

like image 176
Lily Ballard Avatar answered Oct 27 '22 01:10

Lily Ballard


While I agree that overriding automaticallyNotifiesObserversForKey appears to work, but I personally forgo the isExecuting and isFinished properties altogether and instead define executing and finished properties, which, as Kevin suggests, is more consistent with modern conventions:

@property (nonatomic, readwrite, getter = isExecuting) BOOL executing;
@property (nonatomic, readwrite, getter = isFinished)  BOOL finished;

I then write custom setters for these two properties, which do the necessary isExecuting and isFinished notifications:

- (void)setExecuting:(BOOL)executing
{
    [self willChangeValueForKey:@"isExecuting"];
    _executing = executing;
    [self didChangeValueForKey:@"isExecuting"];
}

- (void)setFinished:(BOOL)finished
{
    [self willChangeValueForKey:@"isFinished"];
    _finished = finished;
    [self didChangeValueForKey:@"isFinished"];
}

This yields:

  • a more customary BOOL property declaration;
  • custom setters satisfy the strange notifications that NSOperation requires; and
  • I can now just use the executing and finished setters throughout my operation implementation, without littering my code with notifications.

I must confess that I like the elegance of overriding automaticallyNotifiesObserversForKey, but I just worry about unintended consequences.


Note, if doing this in iOS 8 or Yosemite, you will also have to explicitly synthesize these properties in your @implementation:

@synthesize finished  = _finished;
@synthesize executing = _executing;
like image 41
Rob Avatar answered Oct 26 '22 23:10

Rob