NSPopupButton in view based NSTableView: getting bindings to work - cocoa

Problem Description
I'm trying to achieve something that should be simple and fairly common: having a bindings populated NSPopupButton inside bindings populated NSTableView. Apple describes this for a cell based table in the their documentation Implementing To-One Relationships Using Pop-Up Menus and it looks like this:
I can't get this to work for a view based table. The "Author" popup won't populate itself no matter what I do.
I have two array controllers, one for the items in the table (Items) and one for the authors (Authors), both associated with the respective entities in my core data model. I bind the NSManagedPopup in my cell as follows in interface builder:
Content -> Authors (Controller Key: arrangedObjects)
Content Values -> Authors (Controller Key: arrangedObjects, Model Key Path: name)
Selected Object -> Table Cell View (Model Key Path: objectValue.author
If I place the popup somewhere outside the table it works fine (except for the selection obviously), so I guess the binding setup should be ok.
Things I Have Already Tried
Someone suggested a workaround using an IBOutlet property to the Authors array controller but this doesn't seem to work for me either.
In another SO question it was suggested to subclass NSTableCellView and establish the required connections programmatically. I tried this but had only limited success.
If I setup the bindings as follows:
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
NSView *view = [tableView makeViewWithIdentifier:tableColumn.identifier owner:self];
if ([tableColumn.identifier isEqualToString:#"Author") {
AuthorSelectorCell *authorSelectorCell = (AuthorSelectorCell *)view;
[authorSelectorCell.popupButton bind:NSContentBinding toObject:self.authors withKeyPath:#"arrangedObjects" options:nil];
[authorSelectorCell.popupButton bind:NSContentValuesBinding toObject:self.authors withKeyPath:#"arrangedObjects.name" options:nil];
[authorSelectorCell.popupButton bind:NSSelectedObjectBinding toObject:view withKeyPath:#"objectValue.author" options:nil];
}
return view;
}
the popup does show the list of possible authors but the current selection always shows as "No Value". If I add
[authorSelectorCell.popupButton bind:NSSelectedValueBinding toObject:view withKeyPath:#"objectValue.author.name" options:nil];
the current selection is completely empty. The only way to make the current selection show up is by setting
[authorSelectorCell.popupButton bind:NSSelectedObjectBinding toObject:view withKeyPath:#"objectValue.author.name" options:nil];
which will break as soon as I select a different author since it will try to assign an NSString* to an Author* property.
Any Ideas?

I had the same problem. I've put a sample project showing this is possible on Github.
Someone suggested a workaround using an IBOutlet property to the Authors
array controller but this doesn't seem to work for me either.
This is the approach that did work for me, and that is demonstrated in the sample project. The missing bit of the puzzle is that that IBOutlet to the array controller needs to be in the class that provides the TableView's delegate.

Had the same problem and found this workaround - basically get your authors array controller out of nib with a IBOutlet and bind to it via file owner.

You can try this FOUR + 1 settings for NSPopUpbutton:
In my example, "allPersons" is equivalent to your "Authors".
I have allPersons available as a property (NSArray*) in File's owner.
Additionally, I bound the tableView delegate to File's owner. If this is not bound, I just get a default list :Item1, Item2, Item3

I always prefer the programmatic approach. Create a category on NSTableCellView:
+(instancetype)tableCellPopUpButton:(NSPopUpButton **)popUpButton
identifier:(NSString *)identifier
arrayController:(id)arrayController
relationship:(NSString *)relationshipName
relationshipArrayController:(NSArrayController *)relationshipArrayController
relationshipAttribute:(NSString *)relationshipAttribute
relationshipAttributeIsScalar:(BOOL)relationshipAttributeIsScalar
valueTransformers:(NSDictionary *)valueTransformers
{
NSTableCellView *newInstance = [[self alloc] init];
newInstance.identifier = identifier;
NSPopUpButton *aPopUpButton = [[NSPopUpButton alloc] init];
aPopUpButton.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
[aPopUpButton bind:NSContentBinding //the collection of objects in the pop-up
toObject:relationshipArrayController
withKeyPath:#"arrangedObjects"
options:nil];
NSMutableDictionary *contentBindingOptions = [NSMutableDictionary dictionaryWithDictionary:[[TBBindingOptions class] contentBindingOptionsWithRelationshipName:relationshipName]];
NSValueTransformer *aTransformer = [valueTransformers objectForKey:NSValueTransformerNameBindingOption];
if (aTransformer) {
[contentBindingOptions setObject:aTransformer forKey:NSValueTransformerNameBindingOption];
}
[aPopUpButton bind:NSContentValuesBinding // the labels of the objects in the pop-up
toObject:relationshipArrayController
withKeyPath:[NSString stringWithFormat:#"arrangedObjects.%#", relationshipAttribute]
options:[self contentBindingOptionsWithRelationshipName:relationshipName]];
NSMutableDictionary *valueBindingOptions = [NSMutableDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSAllowsEditingMultipleValuesSelectionBindingOption,
[NSNumber numberWithBool:YES], NSConditionallySetsEditableBindingOption,
[NSNumber numberWithBool:YES], NSCreatesSortDescriptorBindingOption,
[NSNumber numberWithBool:YES], NSRaisesForNotApplicableKeysBindingOption,
[NSNumber numberWithBool:YES], NSValidatesImmediatelyBindingOption,
nil];;
#try {
// The object that the pop-up should use as the selected item
if (relationshipAttributeIsScalar) {
[aPopUpButton bind:NSSelectedValueBinding
toObject:newInstance
withKeyPath:[NSString stringWithFormat:#"objectValue.%#", relationshipName]
options:valueBindingOptions];
} else {
[aPopUpButton bind:NSSelectedObjectBinding
toObject:newInstance
withKeyPath:[NSString stringWithFormat:#"objectValue.%#", relationshipName]
options:valueBindingOptions];
}
}
#catch (NSException *exception) {
//NSLog(#"%# %# %#", [self class], NSStringFromSelector(_cmd), exception);
}
#finally {
[newInstance addSubview:aPopUpButton];
if (popUpButton != NULL) *popUpButton = aPopUpButton;
}
return newInstance;
}
+ (NSDictionary *)contentBindingOptionsWithRelationshipName:(NSString *)relationshipNameOrEmptyString
{
NSString *nullPlaceholder;
if([relationshipNameOrEmptyString isEqualToString:#""])
nullPlaceholder = NSLocalizedString(#"(No value)", nil);
else {
NSString *formattedPlaceholder = [NSString stringWithFormat:#"(No %#)", relationshipNameOrEmptyString];
nullPlaceholder = NSLocalizedString(formattedPlaceholder,
nil);
}
return [NSDictionary dictionaryWithObjectsAndKeys:
nullPlaceholder, NSNullPlaceholderBindingOption,
[NSNumber numberWithBool:YES], NSInsertsNullPlaceholderBindingOption,
[NSNumber numberWithBool:YES], NSRaisesForNotApplicableKeysBindingOption,
nil];
}

Related

Populating NSOutlineView with bindings - KVO adding of items

I've created a small test project to play with NSOutlineView before using it in one of my projects. I'm successful in displaying a list of items with children using a NSTreeController who's content is bound to an Array.
Now while I created this object it took me ages to realize that my array contents would only show up if i created them in my init method:
- (id)init
{
self = [super init];
if (self) {
results = [NSMutableArray new];
NSMutableArray *collection = [[NSMutableArray alloc] init];
// Insert code here to initialize your application
NSMutableDictionary *aDict = [[NSMutableDictionary alloc] init];
[aDict setValue:#"Activities" forKey:#"name"];
NSMutableArray *anArray = [NSMutableArray new];
for (int i; i<=3 ; i++) {
NSMutableDictionary *dict = [NSMutableDictionary new];
[dict setValue:[NSString stringWithFormat:#"Activity %d", i] forKeyPath:#"name"];
[anArray addObject:dict];
}
results = collection;
}
return self;
}
If I put the same code in applicationDidFinishLaunching it wouldn't show the items.
I'm facing the same issue now when trying to add items to the view. My understanding of using the NSTreeController is that it handles the content similar to what NSArrayController does for a NSTableView (OutlineView being a subclass and all). However, whenever I use a KV compliant method to add items to the array the items do not show up in my view.
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSMutableDictionary *cDict = [[NSMutableDictionary alloc] init];
[cDict setValue:#"Calls" forKey:#"name"];
[results addObject:cDict];
[outlineView reloadData];
}
I've also tried calling reloadData on the outlineview after adding an object, but that doesn't seem to be called. What am I missing?
Here's a link to my project: https://dl.dropboxusercontent.com/u/5057512/Outline.zip
After finding this answer:
Correct way to get rearrangeObjects sent to an NSTreeController after changes to nodes in the tree?
It turns out that NSTreeController reacts to performSelector:#selector(rearrangeObjects) withObject:afterDelay:
and calling this after adding the objects lets the new objects appear.

Using an array of arrays to populate NSTableView

I currently have a number of arrays, each containing show title, description and duration. I have them in a further 'shows' array and I'm using this array to populate my NSTableView. What I would like to do is extract the show title from each of my arrays for the first column of my table, the description for the second and so on.
The code I have at the moment though takes the first array in my array of arrays and populates column one, the second array for the second column etc. How would I amend what I have so far to get the table to populate correctly? I've tried to use indexOfObject in place of objectAtIndex however doing so throws and exception. Here's my (simplified) code:
AppDelegate.m
- (void)applicationDidFinishLoading:(NSNotification *)aNotification
{
NSArray *show1 = [[NSArray alloc] initWithObjects:#"Title", #"A description", nil];
NSArray *show2...
NSArray *show3...
NSArray *show4...
self.array = [[NSMutableArray alloc] initWithObjects: show1, show2, show3, show4, nil];
[self.tableView reloadData];
}
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView
{
return [self.array count];
}
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row
{
NSString *identifier = [tableColumn identifier];
if([identifier isEqualToString:#"title"]) {
NSTableCellView *title = [tableView makeViewWithIdentifier:#"title" owner:self];
title.textField.stringValue = [self.array objectAtIndex:0];
return title;
} else if {...}
return nil;
}
Michele Percich's comment is the correct answer: [self.array objectAtIndex:0] will return the first shows array. What you want is "NSArray * show = [self.array objectAtIndex:row]'" to get the show and then "[show objectAtIndex:0]" to get that shows title. Just a suggestion but I'd use an NSArray of NSDictionary's where the keys are the column identifiers. Then you could just use "[self.array objectAtIndex:row] valueForKey:identifier];"
Note also that the method you're overriding expects an instance of NSView (or subclass) to be returned (read the notes in the NSTableView.h header). You may want to use the tableView:objectValueForTableColumn:row: method instead and just return the appropriate NSString (based on the row & column identifier).

Loaded NSNib orders top level objects in no particular order

Here's a piece of code that I'm using to populate view-based NSTableView with data:
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
MyCustomCellView *view = (MyCustomCellView *)[tableView makeViewWithIdentifier:#"MyCustomCellView" owner:self];
if (!view) {
NSNib *cellNib = [[NSNib alloc] initWithNibNamed:#"MyCustomCellView" bundle:[NSBundle mainBundle]];
NSArray *array = nil;
if ([cellNib instantiateNibWithOwner:self topLevelObjects:&array]) {
DLog(#"%#", array);
view = [array objectAtIndex:0];
[view setIdentifier:#"MyCustomCellView"];
}
[cellNib release];
}
MyObject *object = [_objects objectAtIndex:row];
[[view titleTextField] setStringValue:object.title];
return view;
}
The DLog statement prints arrays as following for two consecutive delegate calls:
(
"<MyCustomCellView: 0x7fb2abe81f70>",
"<NSApplication: 0x7fb2ab80cbf0>"
)
(
"<NSApplication: 0x7fb2ab80cbf0>",
"<MyCustomCellView: 0x7fb2abb2c760>"
)
This is output only for two rows out of few hundred so I randomly either get my view displayed correctly or I get unrecognized selector error while calling setIdentifier: for view object when view being objectAtIndex:0 is actually an instance of NSApplication top level object from loaded nib.
Is this a bug in nib loading mechanism or am I doing something wrong with this code?
This thread is a little old, but for what it's worth:
It's not clear whether this is a bug, as the documentation is not specific as to the ordering of the array that's passed back in the topLevelObjects: parameter. However, this snippet has worked for me.
NSArray *arrayOfViews;
BOOL wasLoaded = [[NSBundle mainBundle] loadNibNamed:xibName owner:self topLevelObjects:&arrayOfViews];
NSUInteger viewIndex = [arrayOfViews indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) {
return [obj isKindOfClass:[MyCustomView class]];
}];
self = [arrayOfViews objectAtIndex:viewIndex];

EXC_BAD_ACCESS while working with Core Data

I'm new into Cocoa and am writing a simple app to learn working with Core Data, but it crashes with EXC_BAD_ACCESS. Tried several things and haven't find the solution yet. As I said, I'm not very experienced in Cocoa.
I have followed the usual Core Data tutorials.
This is my Model:
I've added these two entities as NSArrayController in my Nib file and have two NSTableViews with Value Binding to the entity objects.
And here's the code:
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
NSManagedObjectContext *context = [self managedObjectContext];
TaskList *list = [NSEntityDescription
insertNewObjectForEntityForName:#"TaskList"
inManagedObjectContext: context]; // EXC_BAD_ACCESS happens here
[list setTitle:#"Inbox"];
Task *task = [NSEntityDescription
insertNewObjectForEntityForName:#"Task"
inManagedObjectContext: context];
[task setKey:#"Remember the milk"];
[task setList:list];
NSError *error;
if (![context save:&error]) {
NSLog(#"Error: %#", [error localizedDescription]);
}
}
That's it! That's all my program. I am using Xcode 4.2, developing a Mac app, and ARC is enabled.
UPDATE: jrturton asked me to include implementation of [self managedObjectContext]. I didn't write this code, but here's what I found in AppDelegate.h:
#property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
And this is from AppDelegate.m:
#synthesize managedObjectContext = __managedObjectContext;
...
/**
Returns the managed object context for the application (which is already
bound to the persistent store coordinator for the application.)
*/
- (NSManagedObjectContext *)managedObjectContext {
if (__managedObjectContext) {
return __managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (!coordinator) {
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dict setValue:#"Failed to initialize the store" forKey:NSLocalizedDescriptionKey];
[dict setValue:#"There was an error building up the data file." forKey:NSLocalizedFailureReasonErrorKey];
NSError *error = [NSError errorWithDomain:#"YOUR_ERROR_DOMAIN" code:9999 userInfo:dict];
[[NSApplication sharedApplication] presentError:error];
return nil;
}
__managedObjectContext = [[NSManagedObjectContext alloc] init];
[__managedObjectContext setPersistentStoreCoordinator:coordinator];
return __managedObjectContext;
}
Check your managed object model. Make sure all the entity and attribute names are spelled correctly. Also check your object class files and make sure they contain what you expect.
Maybe the debugger does not show you the correct row when crashing: I noticed, that you have a method setKey:, but no attribute called keyin your Task entity. Try setting all the attributes with the dot notation, like list.title = #"Inbox". (This is generally easier to read and avoids similar errors.)
As suggested, before the line inserting the new entity, set a breakpoint and make sure the managed object context is not null.
Finally, perhaps you have to cast your object. insertNewObjectForEntityForName: returns an object of type NSManagedObject, but you are assigning it to a type TaskList. Try adding the cast before you use the object:
TaskList *list = (TaksList *) [NSEntityDescription
insertNewObjectForEntityForName:#"TaskList"
inManagedObjectContext: context];
I had this same issue. I resolved it like Mostafa said above. If you create a project with Core Data enabled, it will automatically create a file for you. Use this .xcdatamodeld file instead of a custom one. If you have one already created, just delete the originally created file and rename your datamodel file to the originally created file name.

Example of how to implement a view-based source list (NSOutlineView) using Cocoa Bindings?

Has anybody found a clear, concise example or guide on how to implement a source list using the view-based NSOutlineView introduced in Lion? I've looked at Apple's example project, but without any sense of direction or explanation, I'm finding it difficult to grasp the concept of exactly how they work.
I know how to use the excellent PXSourceList as a fallback, but would really like to start using view-based source lists instead if at all possible.
You tagged this with the cocoa-bindings tag, so I assume you mean "with bindings." I whipped up a quick example. Start from a new non-document-based Cocoa Application template in Xcode. Call it whatever you like. First I added some code to make some fake data to bind to. Here's what my AppDelegate header looks like:
#import <Cocoa/Cocoa.h>
#interface SOAppDelegate : NSObject <NSApplicationDelegate>
#property (assign) IBOutlet NSWindow *window;
#property (retain) id dataModel;
#end
And here's what my AppDelegate implementation looks like:
#import "SOAppDelegate.h"
#implementation SOAppDelegate
#synthesize window = _window;
#synthesize dataModel = _dataModel;
- (void)dealloc
{
[_dataModel release];
[super dealloc];
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
// Insert code here to initialize your application
// Make some fake data for our source list.
NSMutableDictionary* item1 = [NSMutableDictionary dictionaryWithObjectsAndKeys: #"Item 1", #"itemName", [NSMutableArray array], #"children", nil];
NSMutableDictionary* item2 = [NSMutableDictionary dictionaryWithObjectsAndKeys: #"Item 2", #"itemName", [NSMutableArray array], #"children", nil];
NSMutableDictionary* item2_1 = [NSMutableDictionary dictionaryWithObjectsAndKeys: #"Item 2.1", #"itemName", [NSMutableArray array], #"children", nil];
NSMutableDictionary* item2_2 = [NSMutableDictionary dictionaryWithObjectsAndKeys: #"Item 2.2", #"itemName", [NSMutableArray array], #"children", nil];
NSMutableDictionary* item2_2_1 = [NSMutableDictionary dictionaryWithObjectsAndKeys: #"Item 2.2.1", #"itemName", [NSMutableArray array], #"children", nil];
NSMutableDictionary* item2_2_2 = [NSMutableDictionary dictionaryWithObjectsAndKeys: #"Item 2.2.2", #"itemName", [NSMutableArray array], #"children", nil];
NSMutableDictionary* item3 = [NSMutableDictionary dictionaryWithObjectsAndKeys: #"Item 3", #"itemName", [NSMutableArray array], #"children", nil];
[[item2_2 objectForKey: #"children"] addObject: item2_2_1];
[[item2_2 objectForKey: #"children"] addObject: item2_2_2];
[[item2 objectForKey: #"children"] addObject: item2_1];
[[item2 objectForKey: #"children"] addObject: item2_2];
NSMutableArray* dataModel = [NSMutableArray array];
[dataModel addObject: item1];
[dataModel addObject: item2];
[dataModel addObject: item3];
self.dataModel = dataModel;
}
#end
There's no particular significance to the fake data structure I created, I just wanted to show something with a couple of sub-levels, etc. The only thing that matters is that the key paths you specify in the bindings in Interface Builder line up with the keys in your data (fake data in this case.)
Then select the MainMenu.xib file. In the IB editor, do the following steps:
Use the Object Library (Ctrl-Cmd-Opt-3) to add an NSTreeController to your .xib.
Select the NSTreeController, and using the Attributes Inspector (Cmd-Opt-4) set Key Paths > Children to children (for this example; For your data, this should be whatever returns the array of child objects.)
With the NSTreeController still selected, use the Bindings Inspector (Cmd-Opt-7) to bind the Content Array to the AppDelegate, with a Model Key Path of dataModel
Next use the Object Library (Ctrl-Cmd-Opt-3) to add an NSOutlineView to your .xib.
Arrange it to your satisfaction inside the window (typically the entire height of the window, flush against the left-hand side)
Select the NSOutlineView (note that the first time you click on it, you have likely selected the NSScrollView that contains it. Click on it a second time and you'll have drilled-down to the NSOutlineView itself. Note that this is MUCH easier if you widen the area on the left of the IB editor where all the objects are -- this allows you see the objects as a tree, and navigate and select them that way.)
Using the Attributes Inspector (Cmd-Opt-4) set the NSOutlineView:
Content Mode: View Based
Columns: 1
Highlight: Source List
Using the Bindings Inspector (Cmd-Opt-7) bind "Content" to "Tree Controller", Controller Key: arrangedObjects (This is where the behavior of View-based NSTableView/NSOutlineViews starts to diverge from NSCell-based ones)
In the Object List (mentioned in #6), expand the view hierarchy of the NSOutlineView and select Static Text - Table View Cell.
Using the Bindings Inspector (Cmd-Opt-7) bind Value to Table Cell View, Model Key Path: objectValue.itemName (I've used itemName in the fake data, you would want to use whichever key corresponded to the name of your data items)
Save. Run. You should see a source list, and once you've expanded the nodes with children, you might see something like this:
If you're in the Apple Developer Program, you should be able to access the WWDC 2011 Videos. There's one specifically dedicated to working with View-based NSTableView (and NSOutlineView) and it includes pretty thorough coverage of bindings.
Hope that helps!
Take a look at this example.
SideBarDemo

Resources