I have an inspector pane in my app that contains a bunch of controls. These controls are bound to my model objects through an NSArrayController. Depending on what type of object is selected I am displaying a different set of inspectors (just like how IB works). The inspector controller observes the array controller's selection, so that it can load the required set of inspectors when the selection changes.
The problem is that the old set of inspectors isn't removed apparently. Even through the inspector controller doesn't hold a strong reference to them and they are removed from their superview, they still stick around and log binding errors to the console:
[<Circle 0x102107df0> valueForUndefinedKey:]: this class is not key value
coding-compliant for the key width.
My guess is that the NSArrayController holds a strong reference to the controls because of the bindings. Is this possible? Do I manually have to remove a binding before removing a control from the superview? How do I properly implement an inspector pane like this?
EDIT: The documentation says
Neither the receiver, nor anObserver, are retained.
so I guess bindings should be removed automatically when removing the control, shouldn't it?
The problem is that there isn't a defined order between the inspector controller's response to the selection changing and the various inspector views updating themselves in response to the same thing. So, the "wrong" inspectors for the new array controller selection are still there for a brief time, at least, and trying to access non-existent properties of the element objects.
One fix would be to not rely on key-value observing the array controller selection to switch the set of inspectors in and out. Rather, have a coordinating controller – whichever is controlling the "selected object" based on the user action – clear the set of inspectors before changing the selection and not switching in the new set of inspectors until after it has been changed.
Related
I've a hard time getting my Cocoa application to work as expected. It consists of a toolbar in the main.nib and a custom view in a details.nib file. Now I want the user to select an entry in a NSPopupButton in the toolbar and the content of the custom view should be changed accordingly.
To achive this I've added an ArrayController to my main.nib, showing the following configuration:
Furthermore the Managed Object Context is bound to the Model Key Path delegate.managedObjectContext (it is no document based application).
With this configuration the NSPopupButton works just fine and if I add a Label to the toolbar (also in the main.nib) and bind it's value to the selection (Controller Key), name (Key Value Path) the content is refreshed whenever I change the selection.
The Bindings of the NSPopupButton look like shown in the following screenshot:
So in my details.nib I tried the following to achieve the same effect. I've added an ArrayController, whichs Managed Object Context is also bound to the Model Key Path delegate.managedObjectContext. Also the configuration is exactly the same as shown in the above pictures. I've then added a label and bound it's value to the selection (Controller Key), name (Key Value Path) of this ArrayController.
The problem is that the Label only displays the the initial selection after the application did launched correctly. Afterwards, when I change the selection of my NSPopupButton, the label does not change accordingly.
What are my options to get the ArrayController working accross multiple NIB files?
BTW: I've tried to follow this blog post to get it working but it seems I'm missing something here.
Update:
If I replace the Label in the details.nib by a NSTextField and change the text of it, the changes are reflected in the related NSPopupButton entry. So I guess I made something right, but the main problem remains: I can only edit the entry which was loaded during application startup. Switching to another NSPopupButton entry does not change the text in the NSTextField.
Update 2:
I've created a small sample project with exactly the same configuration and uploaded it on GitHub. So feel free to check it out or create a pull request with a solution approach.
It seems you're missing the fact that, when you create the second array controller on the Details.xib it has no relation to the array controller on the MainMenu.xib. They are two separate instances.
When you change the selection on the PopUp the only array controller affected is the one on MainMenu.xib.
You have several options here:
When you create your DetailViewController pass a reference to the array controller on the Controller and bind to that (don't create a new one on the details.xib)
Just use simple KVO to observe the selection on Controller, and programatically change your label value.
Just use simple KVO to observe the selection on Controller and update the array controller on the DetailsViewController to keep them in sync.
your solution here...
As long as you understand what's going on I'm sure you'll find the best solution to your original problem.
Setup
I have a NSCollectionView. I have a checkbox in the View Prototype. I've successfully set up bindings so the Card Title and action get populated. (image 1, below)
Goal
I'd like, when I click the checkbox, to run a function that accesses the specific CardModel that the View Prototype is already able to access. I'll then manipulate its data accordingly.
Research
I found this article on SO: Get the representedObject values of NSCollectionViewItem NSButton click, which describes my situation pretty well. The answer, unfortunately, is without specific code. Here's what's suggested:
So, first, set the represented object of your button's cell to the
collection view item that owns the button. (You can do this in the nib
editor.) Then, in your action method, get the button's cell, then the
cell's represented object (which is the item), then the item's
represented object.
Seems simple enough, right?
Attempted Solution(s)
I create a method cardCheckBoxClicked: and connect it to the checkbox.
As per the advice above, I connect the button cell's outlet representedObject to Card Collection View Item. (image 2)
I then attempt to get the Card Collection View Item's representedObject in code.
From MainWindowController.h:
-(IBAction)cardCheckBoxClicked:(id)sender
{
CardModel* cModel = [[sender representedObject] representedObject];
NSLog(#"card title: %#",cModel.title);
}
Error
When I click on the checkbox, I get the following error:
-[NSButton representedObject]: unrecognized selector sent to instance 0x6080001581b0
Question!
So - how do I access the button cell's represented object? Did I misunderstand the advice given above? How can I successfully access the data I need?
Images (reference)
bindings example
represented object connection
This here:
-[NSButton representedObject]:
Is you asking the Class NSButton to run the method representedObject. Make sure you distinguish between a Class an an object or instance of that class.
You need to take the actual button, get its button cell, (at least I think that's what you want), and then call representedObject on the cell. If I am understanding you correctly. I never touch interface builder, so here's completely made up code that lines up with what you are asking for.
someObject = [[theButton cell] representedObject];
In addition to CH Buckingham's answer, you should also consider using bindings. You can bind the checkbox's value binding to the collection view item with a model key path which goes through representedObject to some property on your CardModel. (If desired, the key path can keep going through your model object graph.) That will set that property whenever the button is toggled.
I'm having a problem with a custom NSPopUpButtonCell in a table that's instantiated when the table view is populated via bindings and a NSArrayController.
The pop up button cell is created but when attempting to access the outlet by overriding the pop up button cell's setMenuItem:item method it's nil.
Is this the expected behaviour..?
Should another method be used to replace the menu at creation time?
Basically I need the outlet to link back to my controller (NSWindowController) for that document window so I can customize the NSPopUpButtonCell menu accordingly from the custom popup button when it's populated.
A solution using bindings would be even better - but when overriding setObjectValue: I can see it's only never called with a nil parameter.. using a stock NSPopUpButtonCell results in a properly populated pop up menu, though.
(see also Why is NSPopUpButtonCell showing correctly when only setObjectValue:nil is called).
You don't need to override anything to populate an NSPopUpButtonCell in an NSTableView column. The thing to know is that you set the bindings on the NSTableColumn and not on the cell itself. Typically, you would have an NSArrayController in your xib that is bound to an NSArray containing all the options for the pop-up, and then you would select the column with the pop-up cell and go to it's bindings. Like in this screenshot (note the populated Content, Content Objects, and Selected Object bindings in the inspector on the right):
If you want a working example, you can check out this project I whipped up for another StackOverflow question. There's a bunch of unrelated stuff pertaining to making the NSPopUpButtonCell use NSAttributedStrings, but the bindings in the xib constitute a working example of how to bind an NSTableColumn with a pop-up whose options are populated by bindings.
I've got an application here that needs to read in a bunch of data from an external file and display it as a NSPopUpButton in a Cocoa user interface. The catch here is that the data that is being read in needs to have a flag that states if it is considered "hidden" or not.
If the data is hidden, it needs to be added to the NSPopUpButton as an NSMenuItem, but the hidden flag needs to be set to YES so it does not normally appear in the NSPopUpButton menu. If the user holds down a "magic key" on their keyboard (usually ALT, in this case) then those hidden objects need to be unhidden. If the user lets go of the ALT key, then they need to be automatically re-hidden, except for the one that may have been selected -- which would become hidden if another NSMenuItem were chosen.
I'm kind of having a heck of a time figuring this out, actually.
I was wondering if there is a straight forward way of doing this using NSArrayController and an NSPopUpButton, but thus far I have not been able to find anything resembling a solution -- not when it comes to managing the hidden property of the NSMenuItem objects.
Does anyone know how this can be achieved using Cocoa Bindings?
You can wire the popup to an array controller and alter the filter predicate. From an MVC design standpoint, you wouldn't use an attribute like "hidden", which is a view characteristic, but maybe "advanced". Normally, set a filter predicate on your array controller to "advanced = no". Then when the user holds your preferred modifier, remove the predicate. The popup will update automatically. The array controller should be bound to an array property on another object (in your data model). The popup should be bound to arrangedObjects on the array controller.
Short story:
The NSArrayController's selection is being reset whenever setContent is issued. I am wondering if there is a way to turn this off.
Of course, this would be the only acceptable behaviour if I would let NSArrayController use its internal selectionIndexes, because then it wouldn't be able to keep track of both. However, the selectionIndexes are rewired as well, and this part goes off without a hitch. It still feels the need to reset the selection, though.
Update: Ugly hack solution
I've moved this to an answer. I would be pleased to see another more insightful answer though.
Long story:
I have a Cocoa Document-based Application with an inspector panel that is shared between documents (modelled after the TextEdit source code that ships with xcode). Inside the Document class I have an NSMutableArray and an NSMutableIndexSet that are linked up with bindings to an NSArrayController.
The inspector panel is in a separate nib file, and I have two identical NSArrayControllers, one from the main document window, and one from the inspector panel, so that both can interact with the document. This is why I do a manual binding to the selectionIndexes, so that I don't get two separate selections with the two separate NSArrayControllers.
The inspector panel keeps track of which document is being inspected by a:
Document *inspectedDocument;
which is updated whenever the document is switched, or no document has focus at all. An NSObjectController is linked up to inspectedDocument, and the NSArrayController I mentioned before is linked up to that controller.
Now, my problem is that when the inspector panel is in place, and the inspectedDocument is changed, the selection indexes are reset. The problem goes away if I don't use the inspector panel, so I assume it is its NSArrayController that is issuing this reset. I don't have any controls that bind to the selection and could change it (such as a table view).
In the inspector panel's NSArrayController, if I have "Avoid Empty Selection" ticked, the selection resets to the first object, otherwise it resets to no selection, so it is definitely a legitimate resetting of the selection. Actually, I don't even want the inspector panel to ever be able to change the selection, so ideally I would like to establish a read-only binding for that NSArrayController's selection indexes.
I still don't understand why it happens, or if it can be turned off, but the solution I am using is very simple, I just save the selection before I rewire the NSArrayController, and then restore it right after.
This is called when the inspectedDocument is changed and the selection is reset:
NSIndexSet *indexSet = nil;
if (inspectedDocument != doc) {
indexSet = [doc.selectedIndexes copy]; // Backup selection
}
[self setValue:doc forKey:#"inspectedDocument"]; // Selection is reset here
if (indexSet) {
// The following function basically does doc.selectedIndexes = indexSet;
[inspectedDocument selectObjectsAtIndices:indexSet]; // Restore selection
[indexSet release];
}