Manual binding in Cocoa - cocoa

I have an ImageView which shows a lock, informing if an opened file is locked or not. I have 2 images for locked and unlocked cases. I want synchronize the displayed image with boolean value of my object representing an opened file.
To do this I want my ViewController to change the image in my ImageView depending on lock state of object. So both object and ViewController have a property "isLocked".
How can I synchronize them? It is easy in IB but I don't know how to do it programmatically. I tried in initialize method of my ViewController to use:
[ViewController bind:#"value" toObject:[ArrayController selection] withKeyPath:#"isLocked" options:nil];
But it doesn't work. In documentation it is said that I have to expose my binding before using it.
I try to put the following code in initializer method of my object:
[self exposeBinding:#"isLocked"];
But Xcode doesn't recognize this method.
Does somebody have experience with this kind of bindings establishing?

As #nick says, you want Key-Value-Observing.
[arrayController addObserver:self
forKeyPath:#"selection.isLocked"
options:NSKeyValueObservingOptionNew
context:#"this_context"]
Then when isLocked changes the -observeValueForKeyPath:ofObject:change:context: method that you have added to your viewController will be called (as long as you only manipulate isLocked in a KVC compliant way).
The options parameter lets you optionally tweak exactly what conditions will trigger the notification and what data is sent along with the notification. The context parameter is there to help you distinguish between notifications that you registered to receive and notifications your superclass registered to receive. It is optional.
Bindings seem like they might be useful to keep two values in sync. However, this is not what they do at all.
Yes, lots of things seem to give the impression that this is what they do, and there isn't much saying that this isn't what they do, also lots of people believe that this is what they do - but no, you cannot use them for this.
Only a handful of classes support bindings (they are listed here) and then, and this is the important bit, those classes only support binding their named bindings, and these bindings are not instance variables. eg NSTextField has a 'fontFamilyName' binding yet NSTextField does not have a 'fontFamilyName' property or instance variable, even a derived one. NSTextField does have a 'isBordered' property but not a binding - so you cannot bind 'isBordered'.
It does not mean anything to 'bind' an arbitrary property of an arbitrary Class.

Yes, you can bind two arbitrary values, the following code works just fine:
#import <Cocoa/Cocoa.h>
#interface SomeObject : NSObject
#property (retain,nonatomic) id someValue;
#end
#implementation SomeObject
#end
int main()
{
SomeObject *source=[SomeObject new];
SomeObject *target=[SomeObject new];
[target bind:#"someValue" toObject:source withKeyPath:#"someValue" options:0];
[source bind:#"someValue" toObject:target withKeyPath:#"someValue" options:0];
[source setSomeValue:#(42)];
NSLog(#"target: %#",[target someValue]);
[target setSomeValue:#(22)];
NSLog(#"source: %#",[source someValue]);
return 0;
}
As far as I can tell, the problem is the bit [ArrayController selection]. The first problem is that ArrayController is (or should be) a class, and getting the class's selection is probably pointless. The other problem is that even if this were an instance, you would be binding to the selection at the time of the call, which is almost certainly not what you want. You want to track the current selection as it changes.
So what you want is probably something like the following:
[myViewController bind:#"value" toObject:myArrayController withKeyPath:#"selection.isLocked" options:nil];

Related

Programmatically update a TableView that is governed by Cocoa Bindings

I'm fairly new to obj-c and cocoa so please bear with me:
I have a NSTableView set up with cocoa bindings which works as expected with the simple -add -remove, etc methods provided by an instance of NSArrayController in my nib. I would like to programmatically add objects to the array that provides content for this controller (and hence for the table view) and then update the view accordingly.
I current have a working method for adding a new object to the array (verified by NSLog) but I can't figure out how to update the table view.
So: How do I update the bound tableview? (ie, after I have programmatically added objects to my array). I'm essentially after some view refreshing code like [view reloadData] in glue code, but I want it to work with the bindings I have in place.
Or is there a KVC/KVO related solution to this problem?
Code Details:
AppController.h
#interface AppController : NSObject
#property NSMutableArray *clientsArray;
-(IBAction)addClientFooFooey:(id)sender;
#end
AppController.m (note, I also have the appropriate init method not shown here)
#implementation AppController
...
-(IBAction)addClientFooFooey:(id)sender{
[self.clientsArray addObject:[[Client alloc] initWithFirstName: #"Foo" andLastName:#"Fooey"]];
//Need some code to update NSTableView here
}
#end
Client.h just simply defines two properties: firstName and lastName. The 2 columns of an NSTableView in my mainmenu.nib file are appropriately bound to these properties via an array controller bound to my AppController instance.
On a side note/as an alternative. How could I add functionality to the existing NSArrayController method -add, ie, something like: -addWithFirstName:andLastName and still have this compatible with bindings?
You have two main options for doing this provided your array controller is bound to clientsArray.
The first way is to just use the array controller's addObject: method instead of adding objects directly to clientsArray.
The other way is to keep your current addClientFooFooey: method but wrap your existing code with these two lines:
[self willChangeValueForKey:#"clientsArray"];
[self didChangeValueForKey#"clientsArray"];
This tells the KVO system that you are making a change to the array so it will go and look at it again.
The first option is the most straightforward, but if for some reason you need to update the array directly just let KVO know you are doing it.

textShouldEndEditing does not get called in NSTableView

When a user adds a new managed object, it shows up in a table, which scrolls down to the new entry, and the name of the new object (a default value) goes into editing mode.
I need to check if the name of the new object is unique in the datastore, so I can't use a formatter for this. I think the perfect moment where I should validate this, is whenever the user tries to commit the entry's name value, using textShouldEndEditing:.
I subclassed NSTableView and overrid following methods, just to be able to check in the log if they get called.
- (BOOL)textShouldEndEditing:(NSText *)textObject {
NSLog(#"textSHOULDendEditing fired in MyTableView");
return [super textShouldEndEditing:textObject];
}
- (BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor {
NSLog(#"control:textShouldEndEditing fired in MyTableView");
return YES;
}
- (void)textDidEndEditing:(NSNotification *)aNotification {
NSLog(#"textDIDEndEditing fired in MyTableView");
}
textDidEndEditing: gets called fine, but textShouldEndEditing: does not.
In the NSTableView Class Reference, under Text Delegate Methods, both methods textShouldEndEditing: and textDidEndEditing: are listed. Someone please explain why one gets called and the other doesn't.
I think the NSTableView acts as the delegate for an NSTextField that gets instantiated as a black box delegate for the NSTextFieldCell. So what is referred to as delegate methods in the NSTableView Class Reference, actually implement the text manipulating methods for the NSTextField object.
I tried to declare the NSTextFieldCell as an outlet in my NSTableView. I also tried to declare several protocols in the NSTableView.
#import <AppKit/AppKit.h>
#import <Cocoa/Cocoa.h>
#interface MyTableView : NSTableView <NSTextDelegate, NSTextFieldDelegate, NSControlTextEditingDelegate, NSTableViewDelegate, NSTableViewDataSource> {
}
#end
Don't laugh, I even tried to declare my table view as its own delegate :P
After banging my head one entire day on this issue without finding any conclusive answer in Apple documentation, I decided to share the solution I've found in case somebody else struggles with the same problem.
According to the documentation, as the original poster mentioned, the methods control:textShouldBeginEditing and control:textShouldEndEditing of NSControlTextEditingDelegate should be called directly on the delegate:
This message is sent by the control directly to its delegate object.
Furthermore, a Technical Q&A was issued by Apple with the title Detecting the start and end edit sessions of a cell in NSTableView where it's clearly stated the following:
A: How do I detect start and end edit sessions of a cell in NSTableView?
In order to detect when a user is about to start and end an edit session of a cell in NSTableView, you need to be set as the delegate of that table and implement the following NSControl delegate methods:
- (BOOL)control:(NSControl *)control textShouldBeginEditing:(NSText *)fieldEditor;
- (BOOL)control:(NSControl *)control textShouldEndEditing:(NSText *)fieldEditor;
The table forwards the delegate message it is getting from the text view on to your delegate object using the control:textShouldEndEditing: method. This way your delegate can be informed of which control the text view field editor is acting on its behalf.
I found nothing in Apple's documentation stating anything different and if someone does, a documentation pointer would really be appreciated.
In fact, this appears to be true if a cell-based NSTableView is being used. But as soon as you change the table to a view-based table, the delegate method is not called any longer on the table delegate object.
A Solution
However, some heuristic tests I performed showed that those delegate methods get called on a view-based table delegate if (and as far as I know: and only if):
The table delegate is set.
The delegate of the editable control is set.
If you remove either delegate, the methods of the NSControlTextEditingDelegate protocol will not be called.
What's unexpected according to the (only) documentation is setting the delegate of the editable control. On the other hand setting the delegate object to receive delegate notifications sounds rather intuitive to me, and that's why I tried in the first place. But there's a catch! The curious thing, though, is that that's not sufficient. If the table delegate is removed, the NSControlTextEditingDelegate methods will not be called even if the delegate of the editable control is set (which is the weirdest thing to me).
Hope this helps somebody else not to lose time on this issue.
in your question you mention the insertion of a "managed object" and that was the problem. It seems that you are using a view based table, but the textShouldEndEditing: method only gets called for cell based tables.
I overrid -(void)awakeFromInsert; in the (subclassed) managed object, to construct a unique default value for the name-property.
Also, I ended up not overriding the -(BOOL)textShouldEndEditing: method in the table view. Instead, I check if a newly entered name-property is unique in the (subclassed) managed object's -(BOOL)validate<Key>:error:.
Together, the above two strategies result in unique name-properties in all managed objects.
Maybe I could have forced the NSTextFieldCell to go into editing mode, resulting in -(BOOL)textShouldEndEditing: to get called every time.
Some remarks though:
It seems -(BOOL)textShouldEndEditing: returns NO when the -(BOOL)validate<Key>:error: returns NO.
Both -(BOOL)textShouldEndEditing: and -(BOOL)validate<Key>:error: methods are called only when the user actually makes changes to the property.

Converting self-releasing objects to ARC

OK, so Apple brought ARC to us, which is great. After refactoring my Application to ARC almost everything works fine and it is a lot easier now to develop and maintain.
There is just one problem I still can't figure out.
My job management program shows different detail information of proposals, orders and so on in their own windows. So I have a special class where WindowControllers gets allocated and initiated with initWithWindowNibName and then the window is shown with showWindow:
DetailWindowController *proposalWindowController = [[DetailWindowController alloc] initWithWindowNibName:#"ThePorposalWindow"];
[proposalWindowController showWindow:nil];
Before ARC the Instance of the WindowController did the release like shown in the documentation:
- (void)windowWillClose:(NSNotification *)notification
{
[self autorelease];
}
But now with ARC this is not possible anymore and what makes it even worse, in my special class where the WindowController is allocated and initiated, the same windowController is released by ARC because there is no pointer to the windowController.
My idea was to copy the windowController into an mutuable array:
[proposalWindowArray addObject:proposalWindowController];
[[proposalWindowArray lastObject] showWindow:nil];
And in the windowControllers delegate method windowWillClose I post a notification to my special class:
- (void)windowWillClose:(NSNotification *)notification
{
[[NSNotificationCenter defaultCenter] postNotificationName:#"ProposalWindowWillClose" object:[[self window] windowController] userInfo:nil];
}
In my special class I listen to the notification and remove the object from the array:
- (void) proposalWindowWasClosed: (NSNotification *) notification
{
[proposalWindowArray removeObjectIdenticalTo:[notification object]];
}
It works, but I still do not believe that this is the correct way.
Does anybody has the same problem or a tip to make it better?
I'd probably use a delegate approach rather than notifications. Generally it is better to have an external object that keeps track of the open windows. Self-retaining objects, like your old system, break the basic points of object ownership and make it hard to find things (such as "give me a list of open windows"). Non-Singletons that are just "floating" out there often come back to bite you in your architecture (I've had to fix this often enough).
That said, sometimes self-ownership is at least convenient, and at worst not-the-end-of-the-world. So self-own. The only difference is that you need to do it explicitly rather than matching a leak and an over-release (which is what your old code was doing).
Create a private strong property. Assign self to it. That will create a retain loop that will keep you around until you set the property to nil.
I think your alternative approach should be correct, but I don't think you need the second notification. You should be able to do:
- (void)windowWillClose:(NSNotification *)notification
{
[proposalWindowArray removeObjectIdenticalTo:self];
}
Assuming the "proposalWindowArray" is a static NSMutableArray.
Without hacks, there is no elegant way to keep an object retained other than having a strong reference to it in some other object. For example, you could keep a static NSMutableArray/NSMutableSet, add your controller there, and remove it in windowsWillClose:. This will be shorter than posting a notification. To make this reusable, create a WindowControllerRegistry singleton with an array, where you add controllers like this one, and which will automatically listen to NSWindowWillCloseNotification and remove them from its array thus releasing ownership.
As a quick workaround, you could perform retain/autorelease calls from non-ARC file:
my_retain(self);
my_autorelease(self);
// ArcDisabled.mm
void my_retain(id obj) { [obj retain]; }
void my_autorelease(id obj) { [obj autorelease]; }
I had this same issue when I switched to ARC. Your solution works, but you're making it too complicated. You can essentially do what you were doing before by having the window release itself when it closes, but in an ARC compatible manner.
The solution is to simply create a property of your class within the class itself. For your example, in DetailWindowController, you would add the following property:
#property (strong) DetailWindowController *theWindowController;
Then when you create the window with your code above, add one line like so:
DetailWindowController *proposalWindowController = [[DetailWindowController alloc] initWithWindowNibName:#"ThePorposalWindow"];
[preferenceController setTheWindowController:proposalWindowController];
[proposalWindowController showWindow:nil];
Then finally, to have ARC release the window when it is closed like you did with the autorelease pre-ARC, in the DetailWindowController class, simply do:
- (void)windowWillClose:(NSNotification *)notification
{
// Let ARC tear this down and clean it up
[self setTheWindowController:nil];
}

Call IBAction in different class

My document based application has a window with a tableview. The tableview has a datasource which points to a class of type NSObject (called HopBill) which includes a NSMutableArray (aHopBill) and the needed tableview methods. So far so good.
For adding rows to the tableview I've added a sheet which is controlled from a NSWindowController (called HopBillSheetController). When pressing the OK button in the sheet. I actually need to do two IBActions (which is not possible): Add the row to the array of the tableview and close the sheet. I can connect the OK button in the sheet to the NSWindowController (to close the sheet) or connect it to the NSObject (to add the row to the array). But I want both :-)
Is it possible to call the IBAction in the NSWindowController from the NSObject? Or is there another way to do this?
I'm quite a beginner to Cocao and Objective-C, so please be gentle :-)
If your sheet is a nib/xib with an NSPanel, the call to close it is simply [panel close]; Assuming your window controller has a property for the panel, you can put the close code at the end of its row-adding IBAction. Or you could have the IBAction itself call another method if you prefer.
If your panel is running modal, you might need to stopModal too. (That's what's needed if everything stays frozen after the panel closes; otherwise never mind.)
Assuming hopBill, your data source, is a property of the window controller, any IBAction you write in the window controller also has access to hopBill; it can do everything you need.
So add a single IBAction to the window controller and connect the panel's OK button to it. That ought to work.
As for calling an IBAction from somewhere other than a control in a nib, yes, you can do that. Use a reference to the control as the sender arg, or nil if the IBAction doesn't use the sender arg.
You could also create your panel programmatically, or use NSAlert. But it sounds like your current setup is simpler -- and therefore better.
Take a look at this h file for an app controller: Apple's ClockControl example
The NSMutableArray *appointments property is the actual data source that will be used by the NSTableViewDataSource protocol methods. The IBAction "addAppointment" can access "appointments" directly: [self.appointments addObject:whatever atIndex:whatever];
The ClockControl example could be modified to use HopBill. You would import its declarations up top: #import "HopBill.h" And then instead of the "appointments" property, it would declare HopBill *hopBill; And "addApointment" would access HopBill's mutable array (aHopBill) like this: [self.hopBill.aHopBill addObject:whatever atIndex:whatever];
Why you can’t send messages to hopBill:
First, because although you declare it, you never initialize it. You have:
HopBill *hopBill;
[self.hopBill.aHopBill addObject: bHopAdditionAtInit];
It should be:
HopBill *hopBill = [[HopBill alloc] init];
[hopBill.aHopBill addObject: bHopAdditionAtInit]; // “self” won’t work here
Second, you’re declaring it inside an IBAction method, (doneHopBillSheet:), so it’s a local variable, accessible only within that method. If HopBill is holding your table’s data source cache, it should be a property of the controller which implements the NSTableViewDataSourceProtocol methods.
In your HopBill interface, you declare the aHopBill array to be a property, and you initialize it in HopBill’s init method (you should also release it in HopBill’s dealloc method). You need to do the same thing for the controller — it should have an instance of HopBill as a property, and that instance should be initialized in the controller’s init method.
If you want HopBillController to manage the tableview, its interface declaration should look like this:
#interface HopBillSheetController : NSWindowController <NSTableViewDelegate, NSTableViewDataSource> {
…
}
And, then, of course, you have to implement the relevant NSTableViewDelegate and NSTableViewDataSource methods.
Also, the controller must have an IBOutlet property for the tableview itself, and in the controller’s awakeFromNib method, it has to assign itself as delegate and datasource:
[self.tableview setDelegate:self];
[self.tableview setDataSource:self];
(The self-dot syntax assumes you’ve set up #property and #synthesize code for tableview.)
The IBAction method that adds items to your table must be in that controller class, or in a class that has a property which is an instance of the controller class. Then the IBAction method will have access to the aHopBill array and can add the new object to the array, after which it will call [tableView reloadData], which will in turn trigger the tableview protocol methods and update the table.
Now, that means that the xib containing the tableview has to have the controller as its file’s owner. Since you’re using NSDocument, I suspect that, instead, you would put the tableview outlet in the NSDocument subclass. And you would give that doc subclass a property which is an instance of the controller. The IBAction methods would also be in the doc subclass, and so they would have access to the controller and its HopBill property. Or maybe you would simply make the doc subclass the controller, rather than using the separate HopBillSheetController class. I’m not sure about the NSDocument stuff. But, remember, the IBAction method can itself call other methods, as long as it has access to instances of the classes in which those methods are declared.
Apple has an example using both the tableview delegate and datasource protocol methods. Go to this link and download the sample code: tableview example
It looks like a nice app. Good luck.

How to use NSWindowDidExposeNotification

I am trying to update another windows when the one becomes visible. So I found the NSWindowDidExposeNotification and tried to work with it, so I wrote in my awakeFromNib:
// MyClass.m
- (void)awakeFromNib {
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
selector:#selector(mentionsWindowDidExpose:)
name:NSWindowDidExposeNotification
object:nil];
}
and implemented the method
// MyClass.h
- (void)mentionsWindowDidExpose:(id)sender;
// MyClass.m
- (void)mentionsWindowDidExpose:(id)sender {
NSLog(#"test");
}
But it never gets called which is odd. What do I do wrong here?
Generally speaking, you would set up your controller as the window's delegate in order to receive these notifications, like so:
// MyClass.m
- (void)awakeFromNib {
// note: this step can also be done in IB by dragging a connection
// from the window's "delegate" property to your `MyClass` object
[window setDelegate:self];
}
- (void)windowDidExpose:(NSNotification *)notification {
NSLog(#"test");
}
Although, after reading here and here, windowDidExpose may not be your best bet. I would recommend trying the windowDidBecomeKey delegate method instead. That one is posted whenever your window gains "focus" (starts responding to user input) which may be the right time to show your second window.
Update: (in response to comments)
Apple's documentation (quoted below) indicates that NSWindowDidExposeNotification is only valid for nonretained windows, which, according to the posts that I linked above, are quite uncommon.
NSWindowDidExposeNotification
Posted whenever a portion of a nonretained NSWindow object is exposed, whether by being ordered in front of other windows or by other windows being removed from in front of it.
The notification object is the NSWindow object that has been exposed. The userInfo dictionary contains ... the rectangle that has been exposed.
On a higher level, NSNotification objects are simply packages of data that get passed around between Cocoa classes and NSNotificationCenter objects. NSNotificationCenter objects are controllers that manage these packages of data and send them out to observers as required. There is usually no need to trap notifications directly. You can simply use KVC/KVO or pre-defined delegates in your classes and Cocoa handles all of the dirty details behind the scenes.
See Notification Programming Topics and Key Value Coding Programming Guide if you want to know more.

Resources