Binding single NSCell to multiple values - objective-c

I've already killed a day on this subject and still got no idea on how could this be done in a correct way.
I'm using NSOutlineView to display filesystem hierarchy. For each row in the first column I need to display checkbox, associated icon and name of the file or directory. Since there's no standard way to make this, I've subclassed NSTextFieldCell using both SourceView and PhotoSearch examples, binding value in IB to name property of my tree item class though NSTreeController. I'm using drawWithFrame:inView: override to paint checkbox and image, forwarding text drawing to super. I'm also using trackMouse:inRect:ofView:untilMouseUp: override to handle checkbox interaction.
Everything was fine up until I noticed that once I press mouse button down inside my custom cell, cell object is being copied with copyWithZone: and this temporary object is then being sent a trackMouse:inRect:ofView:untilMouseUp: message, making it impossible to modify check state of the original cell residing in the view.
Since the question subject is about binding, I thought this might be the answer, but I totally don't get how should I connect all this mess to function as expected. Tried this:
[[[treeView outlineTableColumn] dataCell] bind:#"state"
toObject:treeController
withKeyPath:#"selection.state"
options:nil];
but didn't succeed at all. Seems like I'm not getting it.
May this be a completely wrong way I've taken? Could you suggest a better alternative or any links for further reading?
UPD 1/21/11: I've also tried this:
[[[treeView outlineTableColumn] dataCell] bind:#"state"
toObject:treeController
withKeyPath:#"arrangedObjects.state"
options:nil];
but kept getting errors like "[<_NSControllerTreeProxy 0x...> valueForUndefinedKey:]: this class is not key value coding-compliant for the key state." and similar.

You bind a table (or outline) column's value, not an individual data cell's state. The data cell's object value is set to the current row/col's value then drawn so you don't have potentially thousands (or millions?) of cells created for no good reason.
Further, you want the tree or array controller's arrangedObjects, not its selection.
Bind the column's value to the tree controller's arrangedObjects as the controller key, and "state" as the model key path in IB; or #"arrangedObjects.state" in code as above.

Okay, I've managed to do what I needed by binding columns's value to arrangedObject's self (in IB) and overriding cell's setObjectValue: so that it looks like:
- (void) setObjectValue:(id)value
{
if ([value isMemberOfClass:[MyNodeClass class]])
{
[super setObjectValue:[value name]];
[self setIcon:[value icon]];
[self setState:[value state]];
}
else
{
if (!value)
{
[self setIcon:nil];
[self setState:NSOffState];
}
[super setObjectValue:value];
}
}
Actual state change is performed within another class, connecting its method to cell's selector (in IB) which I call using
[NSApp sendAction:[self action] to:[self target] from:[self controlView]];
from cell's trackMouse:inRect:ofView:untilMouseUp:. This another class'es method looks like this:
- (IBAction) itemChecked:(id)sender
{
MyNodeClass* node = [[sender itemAtRow:[sender clickedRow]] representedObject];
if (node)
{
[node setState:[node state] == NSOnState ? NSOffState : NSOnState];
}
}

Related

Set NSButton enabled based on NSArrayController selection

OK, so I've set up an NSTableView bound to NSArrayController.
Now, I also have an NSButton which I want to make it "enabled" when there is a selection, and disabled when there is nothing selected.
So, I'm bind NSButton's Enabled to the Array Controller's selection with a value transformer of NSIsNotNil.
However, it doesn't seem to be working.
Am I missing anything?
Regardless of whether or not anything is selected, the selection property of NSArrayController returns an object (_NSControllerProxyObject). This is why your binding isn't working the way you expect, because selection will never be nil. Instead, I bind to selectionIndexes, rather than selection, and have a value transformer called SelectionIndexesCountIsZero implemented like so:
#interface SelectionIndexesCountIsZero : NSValueTransformer
#end
#implementation SelectionIndexesCountIsZero
+ (Class)transformedValueClass { return [NSNumber class]; }
+ (BOOL)allowsReverseTransformation { return NO; }
- (id)transformedValue:(NSIndexSet *)value { return [NSNumber numberWithBool:[value count] > 0]; }
#end
Incidentally, you can still bind to selection if you wish, but it will require a custom value transformer. Apple state that: If a value requested from the selection proxy [object] using key-value coding returns multiple objects, the controller has no selection, or the proxy is not key-value coding compliant for the requested key, the appropriate marker is returned. In other words, to find out if there is in fact no selection, you need to (i) get access to the proxy object, (ii) call one of the methods of your actual objects, and (iii) test to see if the return value from (ii) is NSNoSelectionMarker. Doing it this way the key method of your value transformer would look like this:
- (id)transformedValue:(id)selectionProxyObject {
// Assume the objects in my table are Team objects, with a 'name' property
return [selectionProxyObject valueForKeyPath:#"name"] == NSNoSelectionMarker ? #YES : #NO;
}
selectionIndexes is the better way since it is completely generic. In fact, if you do this sort of thing a lot it can be a good idea to build up a transformer library, which you can then just import into any project. Here are the names of some of the transformers in my library:
SelectionIndexesCountIsZero
SelectionIndexesCountIsExactlyOne
SelectionIndexesCountIsOneOrGreater
SelectionIndexesCountIsGreaterThanOne
// ...you get the picture
I bind to selectedObjects.#count, rather than selection

Observe changes of instance variable in Objective-c

I have a UITableView with a property(strong, nonatomic)NSMutableArray *currentContent, I also have a property(strong, nonatomic)NSMutableArray *cellHeights to keep track of the cell heights cos the user could expand or collapse each cell. The self.currentContent is set by another controller which load data from a web service, so it will change as the data load, I want to keep both of these variables in sync. As soon as currentContent is updated, I want to update cellHeights. How do I do that?
I tried:
- (void)setCurrentContent:(NSMutableArray *)currentContent{
_currentContent = currentContent;
self.cellHeights = [NSMutableArray arrayWithDefaultHeightsForCellCount:[currentContent count]];
}
But it's not working, cos it will only be set at the first time when I set currentContent, when it's empty. So self.cellHeights currently will stay empty. When there is finally value in self.currentContent, self.cellHeights was not updated.
I've done a similar thing before, with variable cell heights depending on the content from your web-service, and I'd advise that keeping an array of cell heights might not be the best idea.
What I did was to create a 'fake' cell in the viewDidLoad: method, that I use just to calculate cell heights.
Then I use the 'heightForRowAtIndexPath' method to specify how tall cell should be by populating the 'fake' cell with the data for the index path, then finding out how tall that cell is. For example:
#interface MyTableViewController()
#property (nonatomic, strong ) MyCustomTableViewCell *cellForTestingHeight;
#end
#implementation MyTableViewController
- (void)viewDidLoad {
[super viewDidLoad]
self.cellForTestingHeight = [[MyCustomTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSDictionary *myData = [self.data objectAtIndex:indexPath.row];
self.cellForTestingHeight.viewData = myData;
return self.cellForTestingHeight.height;
}
#end
This code assumes that you've created a class called MyCustomTableViewCell which has a method to set the viewData property on it, and that after setting that property you'll be able to tell how tall that cell will be by accessing a height property.
What you need to do is observe changes to the mutable array itself, not your property which references the array. This should be straightforward but unfortunately there is a twist...
OK, let's assume there is no twist and rather than an NSMutableArray your property is of type MyObservableClass. Then to setup the observing you would do something like this (all code is pseudo - i.e. typed into the answer):
- (void)setCurrentContent:(MyObservableClass *)currentContent
{
if(_currentContent)
[_currentContent removeObserver:self forKeyPath:#"myObservableProperty"];
_currentContent = currentContent;
[_currentContent addObserver:self forKeyPath:#"myObservableProperty" ...];
}
Now whenever the currentContent is changed your code stops observing the properties of its old value and starts observing properties of its new value.
Easy, but its not quite that simple for arrays...
The twist, though KVO will inform an observer of additions, deletions and changes to a collection the NSMutableArray class doesn't issue those notifications. You need to use a proxy for the array, and such a proxy is provided by the frameworks - see for example NSArrayController and mutableArrayValueForKey:. However the second part of the twist is that the changes to the array must be done through this proxy - you can't have your client call setCurrentContent and pass any NSMutableArray if you are going to observe it, your client needs to pass in the proxy... Whether you can use this easily in your design you'll have to figure out. You'll need to read Automatic Change Notification in this document and other Apple docs to sort this out.
This sounds more complicated that it needs to be, maybe somebody else will provide a more succinct way of doing this.

NSArrayController ignoring filterPredicate on init

Noob warning!
My app displays data in a standard NSTableView via NSArrayController all via bindings. All cool.
There is a filterPredicate, “completed tasks” which is toggled by a check box which drives a BOOL in the background. This works great when the app is running, filter on, filter off, but if I set the BOOL to NO in either init or awakeFromNib, the NSArrayContoller ignores the filterPredicate, but works later.
I could use the predicate to deliver a filtered array to the AC, but guessed bindings & direct filterPredicate would be better.
The AC is init’d in the NIB file and from looking at the KVO feedback, gets created very early on. The filterPredicate does not exist when the AppController inits, but does by the time we get to awakeFromNIb.
Any ideas what I am missing?
Edit:
This is the method called by the check box action:
- (void)displayClosedJobs {
if (self.showClosedJobs)
{
self.showClosedPredicate=nil;
}
else
{
NSString *predValue=#"Closed";
self.showClosedPredicate=[NSPredicate predicateWithFormat:#"!(DisplayName contains[c] %#)",predValue];
}
[self changeTableSortOrder];}
This is also called on init and awakeFromNib.
Most likely you set the bool variable in your init/awakeFromNib directly like so:
_myBoolVariable = NO;
However, what you have to do to send the correct KVO notifications for bindings to notice the change, is use the setter method explicitly either by
[self setMyBoolVariable:NO];
or by
self.myBoolVariable = NO;
However, it's hard to tell without seeing your initialization code. If this is not the problem you should provide more details about how you initialize the variables and how your bindings are set up.

Update property bound from text field without needing to press Enter

I have a text field and I bind it to an NSString instance variable.
When I type in the text field, it does not update the variable. It waits until I press the Enter key. I don't want to hit Enter every time.
What do I need to change in order to make the binding change value immediately?
By default, the value binding of an NSTextField does not update continuously. To fix this, you need, after selecting your text field, to check the "Continuously Updates Value" box in the Bindings Inspector under the Value heading:
However, most often, what you really want to do is update the property to which the text field is bound when the user has finished editing and presses a button ("Save" or "OK", for example). To do this, you needn't continuously update the property as described above, you just need to end editing. Daniel Jalkut provides an extremely useful implementation of just such a method:
#interface NSWindow (Editing)
- (void)endEditing;
#end
#implementation NSWindow (Editing)
- (void)endEditing
{
// Save the current first responder, respecting the fact
// that it might conceptually be the delegate of the
// field editor that is "first responder."
id oldFirstResponder = [oMainDocumentWindow firstResponder];
if ((oldFirstResponder != nil) &&
[oldFirstResponder isKindOfClass:[NSTextView class]] &&
[(NSTextView*)oldFirstResponder isFieldEditor])
{
// A field editor's delegate is the view we're editing
oldFirstResponder = [oldFirstResponder delegate];
if ([oldFirstResponder isKindOfClass:[NSResponder class]] == NO)
{
// Eh ... we'd better back off if
// this thing isn't a responder at all
oldFirstResponder = nil;
}
}
// Gracefully end all editing in our window (from Erik Buck).
// This will cause the user's changes to be committed.
if([oMainDocumentWindow makeFirstResponder:oMainDocumentWindow])
{
// All editing is now ended and delegate messages sent etc.
}
else
{
// For some reason the text object being edited will
// not resign first responder status so force an
/// end to editing anyway
[oMainDocumentWindow endEditingFor:nil];
}
// If we had a first responder before, restore it
if (oldFirstResponder != nil)
{
[oMainDocumentWindow makeFirstResponder:oldFirstResponder];
}
}
#end
So if for example you had a "Save" button targeting your view controller's method -save:, you would call
- (IBAction)save:(id)sender
{
[[[self view] window] endEditing];
//at this point, all properties bound to text fields have the same
//value as the contents of the text fields.
//save stuff...
}
The previous answer is beautiful, and I learned from it about tricking the Window/View/Document system to end-editing on everything at the programmer's will.
However, the default responder chain behavior (including the preservation of the first responder until the USER moved their focus to something else) is fundamental to the Mac's "look and feel" and I wouldn't mess with it lightly (I swear I did very powerful things in responder-chain manipulation, so I don't say that out of fear.)
In addition - there is even a simpler method - that does not require changing the binding. In the Interface-builder, select the text field, and select the "Attribute Inspector" tab. You'll see the following:
Checking the red-circled "continuous" will do the trick. This option is basic and older even than binding, and its main use is to allow validator object (a whole new story) to validate the text and change it on the fly, as the user types. When the text-field calls validator calls, it also updates bound values.

How to click a checkbox in nstableview and update an object using KVC?

I am trying to learn cocoa and have a few problems with KVC and bindings. I have a nstableview with three columns; "checkbox", "text", "icon". The values of each column is binded to an arraycontroller using KVC. When program is launched the rows and columns are correctly filled into the tableview according to the values in the array. I can click a row and correctly print the content of that row using something like this:
- (IBAction)fileTableViewSelected:(id)sender{
NSInteger r;
NSDate *fModOne;
id object;
r = [[NSNumber numberWithInt:[sender selectedRow]] intValue];
object = [arrayIntersect objectAtIndex:r];
fModOne = [object valueForKey:#"fileModifiedDirOne"];
NSLog(#"Date found in row is %#",fModOne);
}
My problem is when I try to click the checkbox in column one and change the value of the box. Initially, the value of the checkbox is set to 1 using the arraycontroller which works fine, but when I want to change the value of the checkbox of a specific row to 0 by clicking on it the program crashes. When the box is clicked an action is correctly called and this is where I thought I could simply change the value of my objects BOOL by calling:
[object setValue:[NSNumber numberWithBool:NO] forKey:#"doSync"];
My setters and getters for the BOOL doSync is defined as:
#property(nonatomic, readwrite) BOOL doSync;
#dynamic doSync;
- (void)setDoSync:(BOOL) value{
NSLog(#"setting dosync %i", value);
doSync = NO;
}
- (BOOL)doSync{
return doSync;
}
I have searched everywhere for a solution to my problem, but I am unable to find any examples of how to use checkboxes in tableview using KVC and bindings. I appreciate any help I can get on this and I would appreciate any examples I could take a look at.
Cheers and thanks! Trond
You don't need to implement this yourself as an action. Just bind the column through your array controller's arrangedObjects to the doSync property of the model objects.
If you don't want to use Bindings, you still shouldn't implement it as an action. Instead, be the table view's data source and respond to the message the table view will send you to change one of the values.
#dynamic doSync;
There's no reason to have this if you turn around and implement the accessors for that property in the same class.
If this is a managed-object class and the property is an attribute of the entity, then your accessors should send [self willAccessValueforKey:] before and [self didAccessValueForKey:] after accessing the instance variable. If that's all they do, then you should not implement the custom accessors at all; cut them out and have #dynamic alone.
- (void)setDoSync:(BOOL) value{
doSync = NO;
That's not setting the property to the value passed in.