简体   繁体   中英

Cocoa-bindings and KVO

I have a view MyView , and it has images which I want to bind with an array in my AppDelegate .

MyView class

@interface MyView : NSView {
@private
    NSArray *images;
}

@end

+ (void)initialize
{
    [self exposeBinding:@"images"];
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    NSLog(@"Changed!");
}

My AppDelegate

@property (retain) NSArray *images;

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{   
    images = [[NSMutableArray alloc] init];

    [view bind:@"images" toObject:self withKeyPath:@"images" options:nil];
    // [self addObserver:view forKeyPath:@"images" options:0 context:nil]; // !!!

    MyImage *img = [[MyImage alloc] ...];

    [self willChangeValueForKey:@"images"];
    [[self images] addObject:img];
    [self didChangeValueForKey:@"images"];
    [img release];
}

Without [self addObserver:view forKeyPath:@"images" options:0 context:nil]; the method observeValueForKeyPath: is never called.

Is it necessary to call addObserver: when using bind: ? Does bind: set the KVO? And why doesn't binding work?

What you need is an implemented setter for the images property like below. The most common use-case for this is that you need to invalidate the drawing and request redraw with -setNeedsDisplay:YES .

- (void)setImages:(NSArray *)newImages
{
  if(newImages != images) {
    [images release];
    images = newImages;
    [images retain];
  }

  [self setNeedsDisplay:YES]; // Addition and only difference to synthesized setter
}

You can drop the -exposeBinding: call, since that has only influence on plugins for Interface Builder, and those where lost with the introduction of Xcode 4.

The reason why the -observeValueForKeyPath:ofObject:change:context: message is not send is that for a binding the observer is not the bound-to object. There is another object in the background. (In the stack form a breakpoint you can see that its class is NSEditableBinder.) So it is correct to register as observer from within the view to the view property @"images".

Another way to get notified about a change in the view is to override -setValue:forKey: method. Then you would need to check the key string and see if it was equal to @"images" . But since there are other methods from the KVC protocol like -setValue:forKeyPath: , you would need to be extra careful to not disturb the machinery, ie always call super .

Uh. I just realize that my answer so far assumes the easier case where you replace the whole array. Your question was for an array modification. (You do declare an immutable array property in your example, though, which only allows replacement. So keep it as declared, and my approach so far will work. Below I show the other alternative.)

Ok, lets assume you do this in the app delegate, a replacement:

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{   
    [view bind:@"images" toObject:self withKeyPath:@"images" options:nil];

    MyImage *img = [[MyImage alloc] ...];

    self.images = [NSArray arrayWithObject:img];
    [img release];
}

You don't need to post the change (using willChangeValueForKey: and didChangeValueForKey: , since you go through the declared property. They do that for you.

Now to the other approach where you modify an array. For that you need to use a mutable array property and modify it through an KVO-notifying proxy, like this:

[self mutableArrayValueForKey:@"images"] addObject:img];

This would pick up the change on the sending (bound-to) side. Then it would be transported to the view through the binding machinery, and eventually set using KVC.

There, on the receiving end in the view, you would need to pick up the property change to @"images". That could be done by overwriting the collection accessor method(s) and do more work there, instead of just accepting the the change. But that is a bit complicated, since there are quite a few accessor methods (See docs ). Or, simpler, you could add another observation relationship from within the view.

For that, somewhere in initialization ( -awakeFromNib: for example) of the view:

[self addObserver:self forKeyPath:@"images" options:0 context:nil];

and then:

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary *)change
                       context:(void *)context
{
  [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];

  if([keyPath isEqualToString:@"images"]) {
    [self setNeedsDisplay:YES]; // or what else you need to do then.
  }
}

Note that this last observer relationship has nothing to do with the binding any longer. The value change to the bound property properly arrives at the view without, you just don't realize (get notified).

That should work.

The only way to have observeValueForKeyPath called is to call addObserver . Binding works through a different mechanism.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM