KVC / KVO and bindings: why am I only getting one change notification?

I see some quirky behavior with Cocoa KVC / KVO and bindings. I have an object NSArrayController

, its "content" is associated with NSMutableArray

, and I have a controller registered as a property observer arrangedObjects

on NSArrayController

. With this setup, I expect to get a KVO notification every time the array is changed. However, it appears that the KVO notification is sent only once; the first time the array was changed.

I created a new "Cocoa Application" project in Xcode to illustrate the problem. Here is my code:

BindingTesterAppDelegate.h

#import <Cocoa/Cocoa.h>

@interface BindingTesterAppDelegate : NSObject <NSApplicationDelegate>
{
    NSWindow * window;
    NSArrayController * arrayController;
    NSMutableArray * mutableArray;
}
@property (assign) IBOutlet NSWindow * window;
@property (retain) NSArrayController * arrayController;
@property (retain) NSMutableArray * mutableArray;
- (void)changeArray:(id)sender;
@end

      

BindingTesterAppDelegate.m

#import "BindingTesterAppDelegate.h"

@implementation BindingTesterAppDelegate

@synthesize window;
@synthesize arrayController;
@synthesize mutableArray;

- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
    NSLog(@"load");

    // create the array controller and the mutable array:
    [self setArrayController:[[[NSArrayController alloc] init] autorelease]];
    [self setMutableArray:[NSMutableArray arrayWithCapacity:0]];

    // bind the arrayController to the array
    [arrayController bind:@"content" // see update
                 toObject:self
              withKeyPath:@"mutableArray"
                  options:0];

    // set up an observer for arrangedObjects
    [arrayController addObserver:self
                      forKeyPath:@"arrangedObjects"
                         options:0
                         context:nil];

    // add a button to trigger events
    NSButton * button = [[NSButton alloc]
                         initWithFrame:NSMakeRect(10, 10, 100, 30)];
    [[window contentView] addSubview:button];
    [button setTitle:@"change array"];
    [button setTarget:self];
    [button setAction:@selector(changeArray:)];
    [button release];

    NSLog(@"run");
}

- (void)changeArray:(id)sender
{
    // modify the array (being sure to post KVO notifications):
    [self willChangeValueForKey:@"mutableArray"];
    [mutableArray addObject:[NSString stringWithString:@"something"]];
    NSLog(@"changed the array: count = %d", [mutableArray count]);
    [self didChangeValueForKey:@"mutableArray"];
}

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

- (void)applicationWillTerminate:(NSNotification *)notification
{
    NSLog(@"stop");
    [self setMutableArray:nil];
    [self setArrayController:nil];
    NSLog(@"done");
}

@end

      

And here's the output:

load
run
changed the array: count = 1
arrangedObjects changed!
changed the array: count = 2
changed the array: count = 3
changed the array: count = 4
changed the array: count = 5
stop
arrangedObjects changed!
done

      

As you can see, the KVO notification is only sent the first time (and again when the app exits). Why is this so?

update:

Thanks to orque for pointing out that I should be tied to contentArray

mine NSArrayController

, not just content

. The above code works as soon as this change is made:

// bind the arrayController to the array
[arrayController bind:@"contentArray" // <-- the change was made here
             toObject:self
          withKeyPath:@"mutableArray"
              options:0];

      

+2


source to share


3 answers


First, you must bind to contentArray (not content):

    [arrayController bind:@"contentArray"
             toObject:self
          withKeyPath:@"mutableArray"
              options:0];

      

Then an easy way is to just use arrayController to change the array:

- (void)changeArray:(id)sender
{
    // modify the array (being sure to post KVO notifications):
    [arrayController addObject:@"something"];
    NSLog(@"changed the array: count = %d", [mutableArray count]);
}

      

(in a real-world scenario, you most likely just want the button action to call -addObject :)



Usage - [NSMutableArray addObject] will not automatically notify the controller. I see that you were trying to work around this by manually using willChange / didChange on mutableArray. It won't work because the array itself hasn't changed. That is, if the KVO system asks for a mutableArray before and after the change, it will still have the same address.

If you want to use - [NSMutableArray addObject], you can change the / didChange value on ordered objects:

- (void)changeArray:(id)sender
{
    // modify the array (being sure to post KVO notifications):
    [arrayController willChangeValueForKey:@"arrangedObjects"];
    [mutableArray addObject:@"something"];
    NSLog(@"changed the array: count = %d", [mutableArray count]);
    [arrayController didChangeValueForKey:@"arrangedObjects"];
}

      

There may be a cheaper key that will give the same effect. If you have a choice, I would recommend just working through the controller and leaving notifications up to the base system.

+7


source


A much better way than explicitly posting KVO notifications with integer values โ€‹โ€‹is to implement array accessors and use them. KVO then sends notifications for free.

So instead of this:

[self willChangeValueForKey:@"things"];
[_things addObject:[NSString stringWithString:@"something"]];
[self didChangeValueForKey:@"things"];

      

You would do this:

[self insertObject:[NSString stringWithString:@"something"] inThingsAtIndex:[self countOfThings]];

      



Not only will KVO post a change notice for you, but it will be a more specific notice, being an array insert change rather than a whole array change.

I usually add a method addThingsObject:

that does the above, so that I can do:

[self addThingsObject:[NSString stringWithString:@"something"]];

      

Note that add<Key>Object:

it is not currently a KVC-recognized selector format for array properties (only set properties), whereas insertObject:in<Key>AtIndex:

, therefore, your implementation should be the first (if you choose to) use the latter.

+5


source


Oh, I've been looking for this solution for a long time! Thanks everyone! After getting the idea and playing around, I found another very fancy way:

Suppose I have a CubeFrames object:

@interface CubeFrames : NSObject {
NSInteger   number;
NSInteger   loops;
}

      

My Array contains Cubeframes objects, they are managed by (MVC) with an objectController and displayed in the tableView. The bindings are done in the usual way: the "Content Array" of the objectController is bound to my array. Important: set the "Class Name" of the Controller object to the CubeFrames class

If I add observers like this to my Appdelegate:

-(void)awakeFromNib {

//
// register ovbserver for array changes :
// the observer will observe  each item of the array when it changes:
//      + adding a cubFrames object
//      + deleting a cubFrames object
//      + changing values of loops or number in the tableview
[dataArrayCtrl addObserver:self forKeyPath:@"arrangedObjects.loops" options:0 context:nil];
[dataArrayCtrl addObserver:self forKeyPath:@"arrangedObjects.number" options:0 context:nil];
}

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

      

Now I will actually catch all the changes: adding and removing lines, changing loops or numbers :-)

0


source







All Articles