NSArrayController, creating CoreData entities programmatically, and KVO - cocoa

I have an NSTableView whose NSTableColumn's value is bound to an NSArrayController. The arrayController controls a set of entities in my core data managed object context.
It works well, and when new entities are inserted into the arrayController via UI Actions the tableView selects the new item.
However I would to create new entities into the moc programmatically, then select the new object in the arrayController.
I have tried the following:
Image *newImage = [Image newImage]; // convenience method to insert new entity into mod.
newImage.title = [[pathToImage lastPathComponent] stringByDeletingPathExtension];
newImage.filename = [pathToImage lastPathComponent];
[self.primaryWindowController showImage:newImage];
The showImage: method is as so:
- (void)showImage:(Image *)image
{
[self.imagesArrayController fetch:self];
[self.imagesArrayController setSelectedObjects:#[image]];
}
However, the arrayController doesn't change its selection.
Am I doing it wrong? I assume that the newImage object that I created in the moc is the same as the object that the arrayController is controlling. If that's true, why isn't the arrayController changing its selection?
Hmm - testing that assumption, I have now checked the contents of the arrayController at runtime. The new image is not present - which I assume means that I have 'gone behind the back' of the bindings by manually inserting into the moc...
My newImage convenience method is as so:
+ (Image *)newImage
{
Image *newImage = [NSEntityDescription insertNewObjectForEntityForName:#"Image" inManagedObjectContext:[[CoreDataController sharedController] managedObjectContext]];
return newImage;
}
Is that not KVO compliant?
hmmm - Edit 2...
I assume that it is KVO compliant, as the new image appears in the UI. I'm now thinking that there is a delay between inserting the entity into the moc and the arrayController being informed.
I See from this question New Core Data object doesn't show up in NSArrayController arrangedObjects (helpfully shown to the right of this question by SO) that asking the arrayController to fetch: should help update the arrayController, but that the actual fetch: won't happen until the next time the runloop runs.
Should I delay the selection of the new object using a timer? That seems a little inelegant...

Right - solved it, thanks to this question: New Core Data object doesn't show up in NSArrayController arrangedObjects
I had to call processPendingChanges: on the moc directly after inserting the new object.
So, my new creation convenience method is now:
+ (Image *)newImage
{
Image *newImage = [NSEntityDescription insertNewObjectForEntityForName:#"Image" inManagedObjectContext:[[CoreDataController sharedController] managedObjectContext]];
[[[CoreDataController sharedController] managedObjectContext] processPendingChanges];
return newImage;
}

If you want to do this programmatically, the easiest is to directly add the new entity object to the NSArrayController, as easy as:
[self.imagesArrayController addObject:newImage];
That will do the trick of both adding the object to the controller, and selecting it.
One glitch though - I don't know what view (NSView, UIView) you use to present the NSArrayController's content - but NSTableView won't automatically scroll itself to reveal the newly added item.
I had to defer this (as adding happens in some later runloop) like thus:
NSUndoManager *um = self.managedObjectContext.undoManager;
[um beginUndoGrouping];
[um setActionName:NSLocalizedString(#"New Sample", NULL)];
PMWaterSample *sampleToAdd = [self createNewSample];
[self.samplesController addObject:sampleToAdd];
// Actual addition is deferred, hence we delay the scrolling too, on the main-thread's queue.
dispatch_async(dispatch_get_main_queue(), ^{
[self.samplesController rearrangeObjects];
// bring last row (newly added) into view
NSUInteger selectedIdx = [self.samplesController selectionIndex];
if (selectedIdx != NSNotFound) {
[self.samplesTable scrollRowToVisible:selectedIdx];
}
[um endUndoGrouping];
});
Hopefully that'll help. I never had to force the MOC to processPendingChanges. I still live with the feeling there's a better way to do this, and have the array controller make its embedding UI element scroll and reveal the new item, but I don't know how.

Related

View-based NSTableView + NSButton

I've got a view-based NSTableView, using Cocoa Bindings to change the values of some labels and images in the cell. It all works great. However, I want to add a button to the cell. I've got the button working, but its action method only has the button as sender, which means I have no idea of the content of the cell that the button is in. Somehow I need to store some extra data on the button - at the very least the row index that the button is in. I subclassed NSButton and used my subclass in the cell, but Interface Builder doesn't know about the extra property so I can't bind to it. If I wanted to bind it in code, I don't know the name of the object or keypath that would be passed to it.
How can I get this to work?
You can use rowForView in your action method to get the row value
- (IBAction)doSomething:(id)sender
{
NSInteger row = [_myTableView rowForView:sender];
}
You can use the Identity field in Interface Builder to associate a table cell view from the nib with an instance in your code:
Additionally you have to implement - tableView:viewForTableColumn:row: in your table view's delegate. (Don't forget to connect the delegate in IB)
- (NSView*)tableView:(NSTableView*)tableView viewForTableColumn:
(NSTableColumn*)tableColumn row:(NSInteger)row
{
SSWButtonTableCellView *result = [tableView makeViewWithIdentifier:#"ButtonView" owner:self];
result.button.title = [self.names objectAtIndex:row][#"name"];
result.representedObject = [self.names objectAtIndex:row];
return result;
}
I added representedObject property in my NSTableCellView subclass, which I set in the above table view delegate method.
Your custom table cell view can later use that object in it's action. e.g.:
- (IBAction)doSomething:(id)sender
{
NSLog(#"Represented Object:%#", self.representedObject);
}

Binding a checkbox to Shared User Defaults

I am building a table to display some data in my preferences pane. All of the data lives in NSUserDefaults. There is a checkbox in the table that will enable/disable data for the listed device. The checkbox is the only cell that is editable.
The table is correctly displaying the data from the Shared User Defaults. So I know that I have the table content properly mapped to the correct Shared User Defaults Model Key Path. However, when I toggle the checkbox, the new data is not being written to the defaults at all.
Here is a glimpse at the checkbox setup...
I have tried assigning a selector action to the NSButton (checkbox), thinking that I could set the default programmatically. Oddly enough, the action never gets triggered. I setup a simple action that just did an NSLog. It never got fired when clicking the checkbox.
Update: So that you can see what my defaults data structure look like, here is the output from the defaults command. There isn't really any code behind this table.
{
ClimateDeviceData = (
{
deviceName = Nest;
deviceSetting = "76";
display = 1;
structure = Home;
uuid = d01AA02AB145204VR;
}
);
ClimateLoginAtLaunch = 1;
ClimateMenuBarIconStyle = "Nest Temp Settings";
}
Update #2: At this point I would accept a solution on simply being able to invoke a selector from the Check Box.
Should you not bind the NSButton (checkbox) to the Shared User Defaults Controller instead of what it is pictured, the Table Cell View?
I am doing roughly the same thing in an app. Not exactly, but basic principle is the same. A table populated with bindings, and a button in there (Can be a check box or another button, doesn't matter).
I tried the action on the button, but it didn't work either, so at the end, I used KVC concept.
I use an arrayController in the XIB referencing a mutable array in my code that stores several instances of a custom object that has a status property (boolean).
The view based tableview is bound to the arrayController using the arrangedObjects controller key.
The button is bound to the tableCellView (the arranged objects), using that keyPath: objectValue.status (effectively fetching the status property of the custom object on that line).
In my controller code, I use the following lines to create the mutable array holding the custom objects:
smartApp *appFound = [[smartApp alloc] initWithApplicationIdentifier:key];
if (appFound)
{
[appFound setStatus:[NSNumber numberWithBool:YES]];
[appFound addObserver:self
forKeyPath:#"status"
options:(NSKeyValueObservingOptionNew |
NSKeyValueObservingOptionOld)
context:NULL];
[_appsArray addObject:appFound];
}
I add an observer on the 'status' keypath of that object. And I add the pending code, observing the object for value changes:
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)anObject
change:(NSDictionary *)change
context:(void *)context
{
if ([anObject isKindOfClass:[smartApp class]])
{
if ([keyPath isEqual:#"status"])
{
NSLog(#"Clicked on row: %lu", (unsigned long)[self.appsArray indexOfObject:anObject]);
}
}
}
Once you're in the method, you can do what you want. It will definitely get called, and you get the object matching the line you clicked on, the change dictionary, and the keypath.
Hope that helps
Yeah, it's different for view-based table views. (I'm just learning this myself.) In a view-based table view the Table Cell View's objectValue is the object represented by that row. You bind your text field, checkbox, etc. to keypaths of that objectValue.

Correct way of updating an NSTableView in a Core Data app

I have a Core Data project with an NSTableView where the columns are bound to an NSArrayController. In turn, the controller's content is bound to the main managed object context of the AppDelegate.
I've sub-classed NSTextFieldCell's to provide a more customised way of displaying the data. Everything seems to work well but when I enumerate through the objects in the context and change an attribute, the NSArrayController doesn't seem to pass the new values through to the table. I know they're being changed as the sortDescriptors work and the table organises itself according to the new data but while still displaying the old values. I'm guessing this is down to the recycling of the NSCell's that display the data: although the underlying values have been changed in the Core Data DB they're not re-drawing.
After trying various KVO notifications with [myArrayController willChangeValueFor: aKeyPath] and the corresponding didChange as well as simply [myTable reloadData]. My only solution has been to unhook the managedObjectContext binding of the controller, and re-hook it after processing, like this:
self.hasBackgroundOp = YES;
self.isCancelled = NO; //can get changed with a 'cancel' button in the UI
if (self.managedObjectContext.hasChanges) {
[self. managedObjectContext save:nil];
}
[myArrayController setManagedObjectContext:nil]; //brutal un-hooking of controller
dispatch_group_t dispatchGroup = dispatch_group_create();
dispatch_group_async(dispatchGroup, dispatch_get_global_queue(0, 0), ^{
.
.
// create a thread context, set the persistent store co-ord
// and do some processing on the
// NSManagedObjects that are fetched into an array just
// for the purpose of this thread
.
.
[ctx save:&error];
//and after the processing is finished then refresh the table
dispatch_async(dispatch_get_main_queue(), ^{
[myArrayController setManagedObjectContext:self.managedObjectContext]; //this forces re-drawing of the table
self.hasBackgroundOp = NO;
self.isCancelled = NO;
});
});
This seems really brutal to be doing [myArrayController setManagedObjectContext:nil]; and then setting it again just to refresh the table's contents. I just can't believe I've done this correctly although it works just fine. Could anyone advise of a better way? Thanks!
UPDATE: actually setting the managed object context doesn't solve the problem. It seems to have stopped working. If the code is run twice in a row then the app just hangs. Comment out the -setManagedObjectContext: lines and the code can be run as many times as required.
In response to the comments, my custom cell uses the following to display itself:
NSInteger index = [[self stringValue] integerValue] + 1;
NSAttributedString *cellText = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:#"%lu",index] attributes:attributes];
NSRect textRect;
textRect.origin = cellFrame.origin;
textRect.origin.x += (cellFrame.size.width - cellText.size.width) / 2;
textRect.origin.y += (cellFrame.size.height - cellText.size.height) / 2;
textRect.size.width = cellText.size.width;
textRect.size.height = cellText.size.height;
[cellText drawInRect:textRect];
The array controller is setup in the main nib and has it's managed object context bound to the AppDelegate's. There are no fetch requests or predicates in the nib - a sort descriptor is bound in the AppDelegate's code:
[myArrayController setSortDescriptors:[NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:kINDEX ascending:YES]]];
Think I've found it - I added the following to the end of the threaded processing and executed it on the main thread:
[self.managedObjectContext reset];
[myArrayController fetch:self];
Resetting the MOC apparently clears it so forcing the controller to redraw the cells. The -fetch appears to be necessary otherwise the table goes blank...

Using NSSegmentedControl with CoreData

I have a Core Data app that works to add or remove one of a Client's many Appointments with buttons bound in IB to my appointments ArrayController. The appointments content is derived from whichever Client is selected in a feed list.
I wish to use a SegmentedControl, and as far as I could tell, this requires I programmatically add and remove the objects in appointments. I have successfully managed to add an Appointment using Marcus Zarra’s code from his book Core Data on p54, but I am at a loss to remove a selected Appointment. I am using a custom table cell, which I suspect might be complicating matters.
In short, I wish to programmatically achieve the equivalent of an ArrayController’s remove: method on a selected object.
Can anyone help, please?
Thanks, Martin. My code eventually looked like this.
-(IBAction) notesEditorSegClicked:(id)sender{
int clickedSegment = [sender selectedSegment];
switch (clickedSegment) {
case 0:{ // add new object
NSManagedObject *newNote = [NSEntityDescription
insertNewObjectForEntityForName:#"Note"
inManagedObjectContext:notes.managedObjectContext];
[notes addObject:newNote];
break;
}
case 1:{ // delete selected object
NSArray *objectsToDelete = [notes selectedObjects];
for (NSManagedObject* objectToDelete in objectsToDelete){
[notes.managedObjectContext deleteObject:objectToDelete];
}
break;
}
case 2:{// close view
[self loadClientSummary:sender];
break;
}
}
}
Get the current selection from you ArrayController bound to your UI
- (NSArray *)selectedObjects
delete those objects using the context
-(void) deleteObject:(NSManagedObject*) object
Sample:
NSArray* objectsToDelete = [NSArray arrayWithArray:[arrayController selectedObject]];
for (NSManagedObject* objectToDelete in objectsToDelete)
{
[arrayController.managedObjectContext deleteObject:objectToDelete];
}

How do I update a NSTableView when its data source has changed?

I am working along with Cocoa Programming For Mac OS X (a great book). One of the exercises the book gives is to build a simple to-do program. The UI has a table view, a text field to type in a new item and an "Add" button to add the new item to the table. On the back end I have a controller that is the data source and delegate for my NSTableView. The controller also implements an IBAction method called by the "Add" button. It contains a NSMutableArray to hold the to do list items. When the button is clicked, the action method fires correctly and the new string gets added to the mutable array. However, my data source methods are not being called correctly. Here they be:
- (NSInteger)numberOfRowsInTableView:(NSTableView *)aTableView {
NSLog(#"Calling numberOfRowsInTableView: %d", [todoList count]);
return [todoList count];
}
- (id)tableView:(NSTableView *)aTableView
objectValueForTableColumn:(NSTableColumn *)aTableColumn
row:(NSInteger)rowIndex {
NSLog(#"Returning %# to be displayed", [todoList objectAtIndex:rowIndex]);
return [todoList objectAtIndex:rowIndex];
}
Here is the rub. -numberOfRowsInTableView only gets called when the app first starts, not every time I add something new to the array. -objectValueForTableColumn never gets called at all. I assume this is because Cocoa is smart enough to not call this method when there is nothing to draw. Is there some method I need to call to let the table view know that its data source has changed, and it should redraw itself?
-[NSTableView reloadData];
See NSTableView API reference

Resources