views:

296

answers:

3

Here's the setup: I have a subclass of IKImageBrowserView whose zoomValue property is bound to a key VFBrowserZoomValue in the shared NSUserDefaultsController. I have an NSSlider whose value binding is bound to the same key.

This works perfectly to change the zoomValue of the browser from the slider.

I'm trying to implement -magnifyWithEvent: in my IKImageBrowserView subclass to allow zooming the browser with the pinch gesture on trackpads.

Here's my implementation:

-(void)magnifyWithEvent:(NSEvent *)event
{
  if ([event magnification] > 0) {
    if ([self zoomValue] < 1) {
      [self setZoomValue: [self zoomValue] + [event magnification]];
    }
  } 
  else if ([event magnification] < 0) {
    if ([self zoomValue] + [event magnification] > 0.1) {
      [self setZoomValue: [self zoomValue] + [event magnification]];
    }
    else {
      [self setZoomValue: 0.1];
    }
  }
}

This changes the browser's zoomValue correctly. The problem is that the NSUserDefaults is not updated with the new value.

Elsewhere in the app, I have an implementation of -observeValueForKeyPath:ofObject:change:context: that observes the browser's zoomValue. If I log the values of the browser's zoom, the slider's value and the key in defaults in that method, I see that the browser's zoomValue has not been pushed into NSUserDefaults and the slider hasn't updated.

I've tried surrounding the -magnifyWithEvent: method with calls to -{will,did}ChangeValueForKey to no effect.

A: 

I'm afraid that's the expected behavior, see this FAQ section of the binding documentation. You need to push that manually.

Yuji
A: 

When you're creating a bindings-compatible control (or, in your case, subclassing one), it's up to the control to notify the controller when its value changes. So, what you'll want to do is override

- (void)bind:(NSString *)binding toObject:(id)observableController withKeyPath:(NSString *)keyPath options:(NSDictionary *)options

and keep an eye out for the binding you're interested in. Keep track of observableController and keyPath (as well as the options dictionary if any value transformers will be used). When you update your control's value, you'll need to send

[observableController setValue:newValue forKeyPath:keyPath];

to update the controller with the new value. Key value observing is a one-way street, and in bindings the controls are the observers.

Alex
+3  A: 

The KVO flow for bindings isn't orthogonal; a binding isn't a property, it's a reference to a property. This is the shorthand to remember for how bindings work:

  • KVO is used to communicate changes from model to controller & view.
  • KVC is used to communicate changes from view to controller & model.

Thus when a view with bindings handles events, it needs to propagate changes to the properties its bindings reference itself.

Here's what your code might look like, with a utility method for doing the heavy lifting of propagating changes through bindings:

- (void)magnifyWithEvent:(NSEvent *)event
{
  if ([event magnification] > 0) {
    if ([self zoomValue] < 1) {
      [self setZoomValue: [self zoomValue] + [event magnification]];
    }
  } 
  else if ([event magnification] < 0) {
    if ([self zoomValue] + [event magnification] > 0.1) {
      [self setZoomValue: [self zoomValue] + [event magnification]];
    }
    else {
      [self setZoomValue: 0.1];
    }
  }

  // Update whatever is bound to our zoom value.
  [self updateValue:[NSNumber numberWithFloat:[self zoomValue]]
         forBinding:@"zoomValue"];
}

It's a little unfortunate that ImageKit requires the use of @"zoomValue" to reference the Zoom Value binding of an IKImageBrowserView, most bindings in AppKit have their own global string constant like NSContentBinding.

And here's that generic utility method to propagate the change through the binding:

- (void)updateValue:(id)value forBinding:(NSString *)binding
{
  NSDictionary *bindingInfo = [self infoForBinding:binding];

  if (bindingInfo) {
    NSObject *object = [bindingInfo objectForKey:NSObservedObjectKey];
    NSString *keyPath = [bindingInfo objectForKey:NSObservedKeyPathKey];
    NSDictionary *options = [bindingInfo objectForKey:NSOptionsKey];

    // Use options to apply value transformer, placeholder, etc. to value
    id transformedValue = value; // exercise for the reader

    // Tell the model or controller object the new value
    [object setValue:transformedValue forKeyPath:keyPath];
  }
}

Actually applying placeholders, value transformers, and the like is left as an exercise for the reader.

Chris Hanson