Binding a checkbox to Shared User Defaults - cocoa

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.

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);
}

NSArrayController, creating CoreData entities programmatically, and KVO

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.

NSButton in NSTableCellView: How to find desired objectValue?

I have a view-based NSTableView that is populated through bindings. My textFields & imageViews are bound to the NSTableCellView's objectValue's properties.
If I want to have an edit/info button in my NSTableCellView:
Who should be the target of the button's action?
How would the target get the objectValue that is associated with the cell that the button is in?
I'd ultimately like to show a popover/sheet based on the objectValue.
I found an additional answer: The Answer above seems to assume you're using bindings on your table view. Since I'm kind of a noob I found a way to get the button inside the table view cell.
- (IBAction)getCellButton:(id)sender {
int row = [xmlTable rowForView:sender];
}
This way when you click on the button inside the row, you don't have to have the row selected. It will return the int value of the row to match up with a datasource in an array without bindings.
Your controller class can be the target. To get the object value:
- (IBAction)showPopover:(id)sender {
NSButton *button = (NSButton *)sender;
id representedObject = [(NSTableCellView *)[button superview] objectValue];
}
Or, use a subclass of NSTableCellView, make the cell view the target of the button's action, and call [self objectValue] to get the object.

How to bind NSButton enabled state to a composed condition

This is my situation in Xcode Interface Builder:
There is also an NSArrayController in entity mode which controls the content of the NSTableView. I want to enable the 'Create' button when the NSTableView is empty (as controlled by the NSSearchField) AND when the text in the NSSearchField is not empty. How do I achieve that? Is it possible without programming?
To what KVO compliant values can I bind the 2 enabled conditions of the 'Create' button?
I don't think there's a way to do it entirely in interface builder, but with a small amount of code you can get it working pretty easily. First, make sure your controller (or App Delegate) is set as the delegate of the search field, and that it has IBOutlet connections to the search field, the button and the array controller. Here's how I would implement it:
// This is an arbitrary pointer to indicate which property has changed.
void *kObjectsChangedContext = &kObjectsChangedContext;
- (void)awakeFromNib {
// Register as an observer so we're notified when the objects change, and initially at startup.
[arrayController addObserver:self
forKeyPath:#"arrangedObjects"
options:NSKeyValueObservingOptionInitial
context:kObjectsChangedContext];
}
// This updates the button state (based on your specs)
- (void)updateButton {
BOOL canCreate = (searchField.stringValue.length > 0 &&
0 == [arrayController.arrangedObjects count]);
[createButton setEnabled:canCreate];
}
// This delegate method is called whenever the text changes; Update the button.
- (void)controlTextDidChange:(NSNotification *)obj {
[self updateButton];
}
// Here's where we get our KVO notifications; Update the button.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (kObjectsChangedContext == context)
[self updateButton];
// It's good practice to pass on any notifications we're not registered for.
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
If you're new to bindings some of that may look like Greek, hopefully the comments are clear enough.
I'm SOOO late for this, but came up with another method and just tested it in my app. It works, so I'm going to share it for anyone who will find this question in the future.
Basically what you want to do is to create a property WITHOUT a corresponding value in your controller
#property (readonly) BOOL enableProperty;
This means that there's actually no
BOOL enableProperty;
defined in the header file, or anywhere
then, rather than synthesize it, just write your own getter, and put there your condition
- (BOOL) enableProperty{
return (condition);
}
Third step: anytime there's the chance that your condition changes, notify it.
- (void) someMethod{
//.... Some code
[self willChangeValueForKey:#"enableProperty"];
[Thisline mightChange:theCondition];
[self didChangeValueForKey:#"enableProperty"];
//.... Some other code
}
fourth step: in IB, bind your control's enabled property to this "fake" property.
Enjoy! ;)
You seems to have a window, so presumably you have a controller object which is set as the File's Owner for the NIB file.
Why not declare a boolean property in this controller class, that returns a value based whatever conditions you want ?
#property(readonly) BOOL canCreate;
That you implement :
-(BOOL)canCreate {
// compute and return the value
}
Be sure to send KVO notifications appropriately when the conditions for the creation change.
The last step is to bind the button's enabled binding on the File's Owner canCreate key.

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