NSManagedObject subclass updating custom property - getting KVO errors - cocoa

EDIT: figured out that the NSNumber was a bad type to bind to. Using NSString instead solved the problem
I have a ToDo entity which has a to-one relationship ("title") with the ToDoTitle entity, and the relationship in reverse("todos") is to-many . i.e. a todo can have one title, but a title can have multiple todos.
Now I want to add a separate NSNumber on top of my NSManagedObject for count of the "incomplete todos" i.e. they have a todo.todoStatus == FALSE.
It looks like this:
#interface ToDoTitle : NSManagedObject
#property (nonatomic, strong) NSNumber * displayOrder;
#property (nonatomic, strong) NSString * title;
#property (nonatomic, strong) NSSet *todo; // this is saved in Core Data model
#property (nonatomic, strong) NSNumber *incompleteCount; // this isn't in the Core Data model
I can tweak the incompleteCount getter to return the correct count of items, and this works:
- (NSNumber *) incompleteCount {
[self willAccessValueForKey:#"incompleteCount"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"todoStatus == FALSE"];
NSSet *incomplete = [[self primitiveValueForKey:#"todo"] filteredSetUsingPredicate: predicate];
[self didAccessValueForKey:#"incompleteCount"];
return #([incomplete count]);
}
Now what I really use this for is for Cocoa Bindings, to display this number on a button. The binding works from the NSButton to a NSTableViewCell with the model key path = "objectValue.managedObject.incompleteCount.stringValue"
This also works fine, and displays the correct data initially. I also implement this in TodoTitle to keep track of changes to the 'todo' set:
+ (NSSet*) keyPathsForValuesAffectingIncompleteCount {
return [[NSSet alloc] initWithObjects: #"todo", nil];
}
so now when I add or delete ToDo objects, the count for each title gets updated correctly as well.
What I can't figure out how to update this incompleteCount when I update one of the todo object's todoStatus. I need to do something like this in the ToDo managed object:
- (void) setTodoStatus:(NSNumber *)todoStatus {
[self willChangeValueForKey:#"todoStatus"];
[self setPrimitiveValue:todoStatus forKey:#"todoStatus"];
if (todoStatus.boolValue == YES) {
self.todotitle.incompleteCount = #(self.todotitle.incompleteCount.integerValue - 1);
}
[self didChangeValueForKey:#"todoStatus"];
}
I get this error:
2014-08-03 22:27:26.632 CJ[9487:303] Cannot update for observer
for the key path
"incompleteCount.stringValue" from , most
likely because the value for the key "incompleteCount" has changed
without an appropriate KVO notification being sent. Check the
KVO-compliance of the ToDoTitle class.
with this stack trace:
2014-08-03 22:27:26.634 Contacts Journal[9487:303] (
0 CoreFoundation 0x00007fff8b5c525c __exceptionPreprocess + 172
1 libobjc.A.dylib 0x00007fff8f25be75 objc_exception_throw + 43
2 CoreFoundation 0x00007fff8b5c510c +[NSException raise:format:] + 204
3 Foundation 0x00007fff92c134b0 -[NSKeyValueNestedProperty object:withObservance:didChangeValueForKeyOrKeys:recurse:forwardingValues:] + 1003
4 Foundation 0x00007fff92ba6fd9 NSKeyValueDidChange + 166
5 Foundation 0x00007fff92babbe6 -[NSObject(NSKeyValueObserverNotification) didChangeValueForKey:] + 118
6 CoreData 0x00007fff88542b01 -[NSManagedObject didChangeValueForKey:] + 113
7 CJ 0x00000001000d664e -[ToDo setTodoStatus:] + 318
8 CJ 0x00000001000ff2a9 -[ToDo(ToDo_Category) updateStatusToComplete] + 345
I'm just not sure to update the incompleteTodos object in a KVO-compliant way. I've tried various things, like wrapping the call inside willChangeValueForKey: and didChangeValueForKey: but doesn't seem to help:
- (void) setTodoStatus:(NSNumber *)todoStatus {
[self willChangeValueForKey:#"todoStatus"];
[self setPrimitiveValue:todoStatus forKey:#"todoStatus"];
if (todoStatus.boolValue == YES) {
[self.todotitle willChangeValueForKey:#"incompleteCount"];
self.todotitle.incompleteCount = #(self.todotitle.incompleteCount.integerValue - 1);
[self.todotitle didChangeValueForKey:#"incompleteCount"];
}
[self didChangeValueForKey:#"todoStatus"];
}
I'm not sure what I'm doing wrong here. I've also tried adding a custom setter in TodoTitle, with and without the willChange/didChange calls:
- (void) setIncompleteCount:(NSNumber *)incompleteCount_ {
incompleteCount = incompleteCount_;
}
but still get the same problem.
Any idea what's wrong here?

Well, it turns out that if I change incompleteCount type to an NSString instead of an NSNumber, and turn the integer into a string, it works fine! Must be some problem with NSNumber's stringValue not updating with the binding? I don't know. Wasted half a day on this!

Related

Why won't the data display in my NSTableView(view based)?

I followed the advice here on how to setup a MainWindowController: NSWindowController for my project's single window. I used a Cocoa class to create the .h/.m files, and I checked the option Also create .xib for User Interface. As a result, Xcode automatically hooked up a window, which I renamed MainWindow.xib, to my MainWidowController.
Next, I deleted the window in the default MainMenu.xib file (in Interface Builder I selected the window icon, then I hit the delete key). After that, I was able to Build my project successfully, and my controller's window in MainWindow.xib displayed correctly with a few buttons on it.
Then I tried adding an NSTableView to my MainWindowController's window. In Xcode, I dragged the requisite delegate and datasource outlets for the NSTableView onto File's Owner, which is my MainWindowController, and I implemented the methods in MainWindowController.m that I thought would make the NSTableView display my data:
- tableView:viewForTableColumn:row:
- numberOfRowsInTableView:
Now, when I Build my project, I don't get any errors, but the data doesn't appear in the NSTableView.
My code is below. Any tips are welcome!
//
// AppDelegate.h
// TableViews1
//
#import <Cocoa/Cocoa.h>
#interface AppDelegate : NSObject <NSApplicationDelegate>
#end
...
//
// AppDelegate.m
// TableViews1
//
#interface AppDelegate ()
#property (weak) IBOutlet NSWindow *window;
#property (strong) MainWindowController* mainWindowCtrl;
#end
#implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// Insert code here to initialize your application
[self setMainWindowCtrl:[[MainWindowController alloc] init] ];
[[self mainWindowCtrl] showWindow:nil];
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {
// Insert code here to tear down your application
}
#end
...
//
// MainWindowController.h
// TableViews1
//
#import <Cocoa/Cocoa.h>
#interface MainWindowController : NSWindowController
#end
...
//
// MainWindowController.m
// TableViews1
//
#import "MainWindowController.h"
#import "Employee.h"
#interface MainWindowController () <NSTableViewDataSource, NSTableViewDelegate>
#property (strong) NSMutableArray* employees;
#property (weak) IBOutlet NSTableView* tableView;
#end
#implementation MainWindowController
- (NSView*)tableView:(NSTableView *)tableView
viewForTableColumn:(NSTableColumn *)tableColumn
row:(NSInteger)row {
Employee* empl = [[self employees] objectAtIndex:row];
NSString* columnIdentifier = [tableColumn identifier];
//The column identifiers are "firstName" and "lastName", which match my property names.
//You set a column's identifier by repeatedly clicking on the TableView until only
//one of the columns is highlighted, then select the Identity Inspector and change the column's 'Identifier' field.
NSString* emplInfo = [empl valueForKey:columnIdentifier]; //Taking advantage of Key-Value coding
NSTableCellView *cellView =
[tableView makeViewWithIdentifier:columnIdentifier
owner:self];
NSLog(#"The Table view is asking for employee: %#", [empl firstName]);
[[cellView textField] setStringValue:emplInfo];
return cellView;
}
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
return [[self employees] count];
}
- (void)windowDidLoad {
[super windowDidLoad];
// Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
Employee* e1 = [[Employee alloc] initWithFirstName:#"Joe" lastName:#"Blow"];
Employee* e2 = [[Employee alloc] initWithFirstName:#"Jane" lastName:#"Doe"];
[self setEmployees:[NSMutableArray arrayWithObjects:e1, e2, nil]];
//Test to see if the employees array was populated correctly:
Employee* e = [[self employees] objectAtIndex:0];
NSLog(#"Here is the first employee: %#", [e firstName]);
//I see the output: "Here is the first employee: Joe"
}
- (id)init {
return [super initWithWindowNibName:#"MainWindow"];
}
- (id)initWithWindowNibName:(NSString *)windowNibName {
NSLog(#"Clients cannot call -[%# initWithWindowNibName] directly!",
[self class]
);
[self doesNotRecognizeSelector:_cmd];
return nil;
}
#end
...
//
// Employees.h
// TableViews1
#import <Foundation/Foundation.h>
#interface Employee : NSObject
#property NSString* firstName;
#property NSString* lastName;
- initWithFirstName:(NSString*)first lastName:(NSString*)last;
#end
...
//
// Employees.m
// TableViews1
//
#import "Employee.h"
#implementation Employee
- (id)initWithFirstName:(NSString *)first lastName:(NSString *)last {
if (self = [super init]) {
_firstName = first; //I read that you shouldn't use the accessors in init methods.
_lastName = last;
}
return self;
}
#end
File's Owner(=MainWindowController) connections:
NSTableView connections:
Response to comments:
Here is why calling [self tableView] reloadData] at the end of -windowDidLoad, as suggested in the comments, didn't work:
My _tableView instance variable--created by my #property declaration in MainWindowController.m--doesn't point to anything; therefore calling:
[[self tableView] reloadData]
I think is equivalent to calling:
[nil reloadData]
which doesn't do anything.
I never assigned anything to the _tableView instance variable in the -init method, nor did I assign it a value by dragging an outlet somewhere in Interface Builder. To fix that problem, I selected MainWindow.xib (the controller's window) in the Project Navigator(left pane), and then in the middle pane(Interface Builder), I selected the cube representing the File's Owner(selecting the Identity Inspector in the right pane reveals that the File's Owner is the MainWindowController). Then in the right pane, I selected the Connections Inspector, and it revealed an outlet called tableView, which is the IBOutlet variable I declared in MainWindowController.m.
Next, I dragged from the tableView outlet onto the TableView in the middle pane:
Doing that assigns the NSTableView object to the _tableView instance variable that was created by my #property declaration in MyWindowControler.m:
#property (weak) IBOutlet NSTableView* tableView;
As an experiment, I disconnected the outlet, then commented out the #property declaration for tableview, and the tableView outlet no longer appeared in the Connections Inspector. Also, if I change the declaration from:
#property (weak) IBOutlet NSTableView* tableView;
to:
#property (weak) NSTableView* tableView;
...then the tableView outlet doesn't appear in the Connections Inspector. That experiment answered a couple of questions I had about whether I should declare a property as an IBOutlet or not: if you need to assign one of the objects in Interface Builder to one of your variables, then declare the variable as an IBOutlet.
Thereafter, calling [self tableView] reloadData] at the end of -windowDidLoad succeeds in populating the TableView. However, I have not seen any tutorials that call reloadData, and even Apple's guide does not do that.
So, I am still puzzled about whether calling -reloadData is a hack or it's the correct way to do things.
Without it, your table view sits there blissfully clueless about your
expectation that it should even bother asking its datasource for data.
I assumed that an NSTableView automatically queries its datasource when it is ready to display itself, and that my code needed to be able to provide the data at that time.
I don't see you sending -reloadData to your table view anywhere. Tacking it onto the end of -windowDidLoad would be a good place. Without it, your table view sits there blissfully clueless about your expectation that it should even bother asking its datasource for data.
For all it knows, the data is simply not ready / available, so why would it try? More importantly, when should it try? It'd be rather rude of it to try whenever it pleases, considering the UI may not have finished loading / connecting to outlets, or its datasource may be in a vulnerable state (like teardown during/after dealloc) and sending datasource requests may result in a crash, etc.
Two things:
1st, set some breakpoints on when you set your employees array in windowDidLoad vs. when the table first attempts to populate itself and your numberOfRowsInTableView implementation gets called. If the latter happens before the former, then you'll need to add a reloadData after you create your array.
2nd, I personally always use NSCell instead of NSViews for my tables, so I always implement objectValueForTableColumn in my table's datasource. So I'm not sure if there's something different you need to do when you use NSView objects and implement viewForTableColumn. Is there a reason you're not using NSCell?

MapKit changes annotation view order when map is scrolled in XCode 4.6

I am an experienced iOS developer, but this has really stumped me. I am simultaneously submitting a problem report to Apple.
I'm adding annotations to a MKMapKit map (Xcode 4.6). Each annotation is a MyAnnotation class; MyAnnotation defines a property, location_id, that I use to keep track of that annotation.
The problem is simple: I want the MyAnnotation with a location_id of -1 to appear in front of everything else.
To do this, I am overriding mapView:didAddAnnotationViews: in my MKMapViewDelegate:
-(void)mapView:(MKMapView *)mapView didAddAnnotationViews:(NSArray *)views {
// Loop through any newly added views, arranging them (z-index)
for (MKAnnotationView* view in views) {
// Check the location ID
if([view.annotation isKindOfClass:[MyAnnotation class]] && [((MyAnnotation*)(view.annotation)).location_id intValue]==-1 ) {
// -1: Bring to front
[[view superview] bringSubviewToFront:view];
NSLog(#"to FRONT: %#",((MyAnnotation*)view.annotation).location_id);
} else {
// Something else: send to back
[[view superview] sendSubviewToBack:view];
NSLog(#"to BACK: %#",((MyAnnotation*)view.annotation).location_id);
}
}
}
this works just fine. I have an "add" button that adds an annotation to a random location near the center of my map. Each time I push the "add" button, a new annotation appears; but nothing hides the annotation with the location_id of -1.
** UNTIL ** I scroll!
As soon as I start scrolling, all of my annotations are rearranged (z-order) and my careful stacking no longer applies.
The really confusing thing is, I've done icon order stacking before with no problem whatsoever. I've created a brand new, single view app to test this problem out; sending MKAnnotationView items to the back or front only works until you scroll. There is nothing else in this skeleton app except the code described above. I'm wondering if there is some kind of bug in the latest MapKit framework.
The original problem was in trying to add new annotations when the user scrolled (mapView:regionDidChangeAnimated:). The annotation adds; the mapView:didAddAnnotationViews: code fires; and then the order is scrambled by an unseen hand in the framework (presumably as the scroll completes).
In case you're interested, here is my viewForAnnotation:
-(MKAnnotationView*)mapView:(MKMapView *)mapView viewForAnnotation:(id<MKAnnotation>)annotation {
// Is this an A91Location?
if([annotation isKindOfClass:[MyAnnotation class]]){
MyAnnotation* ann=(MyAnnotation*)annotation;
NSLog(#"viewForAnnotation with A91Location ID %#",ann.location_id);
if([ann.location_id intValue]==-1){
// If the ID is -1, use a green pin
MKPinAnnotationView* green_pin=[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:nil];
green_pin.pinColor=MKPinAnnotationColorGreen;
green_pin.enabled=NO;
return green_pin;
} else {
// Otherwise, use a default (red) pin
MKPinAnnotationView* red_pin=[[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:nil];
red_pin.enabled=NO;
return red_pin;
}
}
// Everything else
return nil;
}
And my class:
#interface MyAnnotation : NSObject <MKAnnotation>
#property (strong, nonatomic, readonly) NSNumber* location_id;
#property (strong, nonatomic, readonly) NSString* name;
#property (strong, nonatomic, readonly) NSString* description;
#property (nonatomic) CLLocationCoordinate2D coordinate;
-(id) initWithID:(NSNumber*)location_id name: (NSString*) name description:(NSString*) description location:(CLLocationCoordinate2D) location;
// For MKAnnotation protocol... return name and description, respectively
-(NSString*)title;
-(NSString*)subtitle;
#end
Could you possibly try the following as a work around:
1) Remove your code in mapView:didAddAnnotationViews:
2) Pickup when the map is moved or pinched (I assume this is what you consider to be a scroll right?) - I do this with gestures rather than the mapView regionChanged as I have had bad experiences such as unexplained behaviour with the latter.
//recognise the paning gesture to fire the didDragMap method
UIPanGestureRecognizer* panRec = [[UIPanGestureRecognizer alloc] initWithTarget:self action:#selector(didDragMap:)];
[panRec setDelegate:self];
[self.mapView addGestureRecognizer:panRec];
//recognise the pinching gesture to fire the didPinchMap method
UIPinchGestureRecognizer *pinchRec = [[UIPinchGestureRecognizer alloc]initWithTarget:self action:#selector(didPinchMap:)];
[pinchRec setDelegate:self];
//[pinchRec setDelaysTouchesBegan:YES];
[self.mapView addGestureRecognizer:pinchRec];
//recognise the doubleTap
UITapGestureRecognizer *doubleTapRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(didPinchMap:)];
[doubleTapRec setDelegate:self];
doubleTapRec.numberOfTapsRequired = 2;
[self.mapView addGestureRecognizer:doubleTapRec];
3) write a custom "plotAnnotations" function to show your annotations. This function will loop through all your annotations and save them in an array "annotationsToShow" ordered by your location id DESC so that your location_id -1 are added last. Use [self.mapView addAnnotations:annotationsToShow]; to display them.
4) Call plotAnnotations in your gestureRecognizer functions
- (void)didDragMap:(UIGestureRecognizer*)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateEnded){
[self plotAnnotations];
}
}
- (void)didPinchMap:(UIGestureRecognizer*)gestureRecognizer {
if (gestureRecognizer.state == UIGestureRecognizerStateEnded){
[self plotAnnotations];
}
}
5) You may need to delete all annotations before displaying the new ones in 3) [self.mapView removeAnnotations:annotationsToRemove];

Crash in objc_retain in method performed with performSelector

I have this strange crash relating to ARC auto-inserting objc_retains in my code.
I have the following two classes:
#interface MenuItem : NSObject
#property (weak, nonatomic) id target;
#property (unsafe_unretained, nonatomic) SEL action;
#property (strong, nonatomic) id object;
- (instancetype)initWIthTarget:(id)target action:(SEL)action withObject:(id)object;
- (void)performAction;
#end
#implementation MenuItem
- (void)performAction
{
if (self.target && self.action)
{
if (self.object)
{
[self.target performSelector:self.action withObject:self.object];
}
else
{
[self.target performSelector:self.action];
}
}
}
#end
#interface Widget : NSObject
- (void)someMethod:(id)sender;
#end
At some point I instantiate a MenuItem as such:
MenuItem *item = [MenuItem alloc] initWithTarget:widget action:#selector(someMethod:) object:nil];
Then elsewhere I invoke performAction on the menu item:
[item performAction];
In the implementation of someMethod I get a crash:
#implementation Widget
- (void)someMethod:(id)sender
{
// EXEC_BAD_ACCESS crash in objc_retain
}
#end
Why is this happening?
The reason for the crash was because I was using the wrong performSelector.
NSObject defines multiple versions of performSelector. The one I was invoking was:
- (id)performSelector:(SEL)aSelector;
However the method I was invoking took an id parameter. Eg:
- (void)someMethod:(id)sender;
Now ARC being the nice safe memory management system that it is tries to ensure that parameters are properly retained during the execution of a method. So even though my someMethod: was empty ARC was producing code that looked like this:
- (void)someMethod:(id)sender
{
objc_retain(sender);
objc_release(sender);
}
The problem with this however was that I was invoking performSelector: and not supplying a value for the sender parameter. So sender was pointing at random junk on the stack. Therefore when objc_retain() was invoked the app crashed.
If I change:
MenuItem *item = [[MenuItem alloc] initWithTarget:widget
action:#selector(someMethod:)
object:nil];
to
MenuItem *item = [[MenuItem alloc] initWithTarget:widget
action:#selector(someMethod)
object:nil];
and
- (void)someMethod:(id)sender;
to
- (void)someMethod;
Then the crash goes away.
Similarly I can also change
[self.target performSelector:self.action];
to
[self.target performSelector:self.action withObject:nil];
if I want to follow the 'standard' form of target-action methods that take a single parameter. The benefit of the second form of performSelector is that if I'm invoking a method that doesn't take a parameter it will still work fine.

Multilevel cocoa bindings

When I bind to a multiple level keypath, say objectValue.person.photo, it does not update when the person changes, only when the photo changes. This would seem to be a problem with only the last key in the path being observed for changes.
Is it possible to observe multiple levels of bindings? For instance, in SproutCore, if you place an asterisk in the path, everything after it will be observed for changes (objectValue*person.photo).
If your bindings are not updating when objectValue.person is changed, then that usually means that whatever object is in objectValue is not Key-Value Observing compliant for the key person. With properly implemented objects, non-leaf mutations along a keyPath work fine. For instance, starting from the base non-document Cocoa Application template, I cooked up the following example:
Header:
#interface Person : NSObject
#property (copy) NSString* name;
#end
#interface Car : NSObject
#property (retain) Person* driver;
#end
#interface SOAppDelegate : NSObject <NSApplicationDelegate>
#property (assign) IBOutlet NSWindow *window;
#property (retain) Car* car;
- (IBAction)replaceCar:(id)sender;
- (IBAction)replaceDriver:(id)sender;
- (IBAction)changeName:(id)sender;
#end
Implementation:
#implementation Person
#synthesize name;
#end
#implementation Car
#synthesize driver;
#end
#implementation SOAppDelegate
#synthesize car = _car;
#synthesize window = _window;
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
Person* person = [[[Person alloc] init] autorelease];
person.name = #"Default Name";
Car* car = [[[Car alloc] init] autorelease];
car.driver = person;
self.car = car;
}
- (IBAction)replaceCar:(id)sender
{
Person* person = [[[Person alloc] init] autorelease];
person.name = #"Replaced Car";
Car* newCar = [[[Car alloc] init] autorelease];
newCar.driver = person;
self.car = newCar;
}
- (IBAction)replaceDriver:(id)sender
{
Person* person = [[[Person alloc] init] autorelease];
person.name = #"Replaced Driver";
self.car.driver = person;
}
- (IBAction)changeName:(id)sender
{
self.car.driver.name = #"Changed Name";
}
#end
Then in the .xib, I added three buttons, calling each of the IBActions and added a label whose value property was bound to App Delegate with a keyPath of car.driver.name
Pushing any of the buttons will cause the bound label to update, despite the fact that only one of them actually modifies the exact value pointed to by the bindings keyPath (car.driver.name). KVO compliance comes for free with standard #synthesized properties, so we get proper updates no matter what level in the keyPath they come from.
In short, bindings work the way you want them to (i.e. they update for changes to non-leaf-node keys in a compound keyPath). There's something in the implementation of the objects in objectValue or person that's deficient and preventing this from working. I would look there.
Also note, in case one of these things is a collection, that observing a collection is not the same thing as observing all the objects in a collection. See this page for more info on that.
PS: Yes, I know the example leaks memory. You can imagine the relevant -dealloc methods for yourself.

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

I'm seeing some quirky behaviour with Cocoa's KVC/KVO and bindings. I have an NSArrayController object, with its 'content' bound to an NSMutableArray, and I have a controller registered as an observer of the arrangedObjects property on the NSArrayController. With this setup, I expect to receive a KVO notification every time the array is modified. However, it appears that the KVO notification is only sent once; the very first time the array is modified.
I set up a brand 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 is 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 once more when the application exits). Why would this be the case?
update:
Thanks to orque for pointing out that I should be binding to the contentArray of my NSArrayController, not just its content. The above posted 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];
First, you should bind to the contentArray (not content):
[arrayController bind:#"contentArray"
toObject:self
withKeyPath:#"mutableArray"
options:0];
Then, the straightforward way is to just use the arrayController to modify 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 scenario you'll likely just want the button action to call -addObject:)
Using -[NSMutableArray addObject] will not automatically notify the controller. I see that you tried to work around this by manually using willChange/didChange on the mutableArray. This won't work because the array itself hasn't changed. That is, if the KVO system queries mutableArray before and after the change it will still have the same address.
If you want to use -[NSMutableArray addObject], you could willChange/didChange on arrangedObjects:
- (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 would give the same effect. If you have a choice I would recommend just working through the controller and leaving the notifications up to the underlying system.
A much better way than explicitly posting whole-value KVO notifications is to implement array accessors and use them. Then KVO posts the notifications for free.
That way, 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 the change notification for you, but it will be a more specific notification, being an array-insertion change rather than a whole-array change.
I usually add an addThingsObject: method that does the above, so that I can do:
[self addThingsObject:[NSString stringWithString:#"something"]];
Note that add<Key>Object: is not currently a KVC-recognized selector format for array properties (only set properties), whereas insertObject:in<Key>AtIndex: is, so your implementation of the former (if you choose to do that) must use the latter.
Oh, I was looking for a long time for this solution ! Thanks to all !
After getting the idea & playing around , I found another very fancy way:
Suppose I have an object CubeFrames like this:
#interface CubeFrames : NSObject {
NSInteger number;
NSInteger loops;
}
My Array contains Objects of Cubeframes, they are managed via (MVC) by an objectController and displayed in a tableView.
Bindings are done the common way:
"Content Array" of the objectController is bound to my array.
Important: set "Class Name" of objectController to class CubeFrames
If I add observers like this in 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, indeed, I catch all the changes : adding and deleting rows, change on loops or number :-)

Resources