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).
Related
I'm trying to implement a controller for my Cocoa NSTableView, which is filled with data of a SQLite database file. The controller implements the NSTableViewDataSource protocol and thus the methods
-(NSInteger)numberOfRowsInTableView:(NSTableView*)tableView {
}
and
-(id)tableView:(NSTableView*)tableView setObjectValue:(id)object
forTableColumn:(NSTableColumn*)tableColumn
row:(NSInteger*)row {
}
which are obviously called quiet often (e.g. if I scroll the table). To provide up-to-date data to both methods I'm executing an NSFetchRequest every time one of these methods is invoked.
The actual problem is related to my IBAction, that adds a new entry to my table. At the end of this method I call the reloadData method on the table view, which, if I'm correct, calls both protocol methods at least one time and then leads to unsorted data in the table. I've figured out that every NSFetchRequest returns this unsorted data until I save the managedObjectContext. But this is not an option (and not even possible) at this time since there is a mandatory field, which needs to be filled out in the table first.
So here are my two questions:
1) Why does my very first fetch request after the insertNewObjectForEntityForName call (and all further requests until I save) result in unsorted data?
2) How can I avoid this behaviour without saving (since I can't without the entered mandatory field)?
Since I'm new to this whole Cocoa and CoreData stuff I will post my complete code to give you a clear understanding what I'm trying to do. Any comments are welcome.
Regards,
Richard
#import "EventTabController.h"
#import "CoreData.h"
#import "Season.h"
#implementation EventTabController
-(id) init {
if(self = [super init]) {
managedObjContext = [[CoreData getInstance] managedObjContext];
}
return self;
}
/**
* Part of the NSTableViewDataSource protocol. This method must return the number of
* elements that are currently to be shown in the table.
*/
-(NSInteger)numberOfRowsInTableView:(NSTableView *)tableView {
NSError *err;
NSInteger numOfRows = [managedObjContext countForFetchRequest:[self createSeasonFetchRequest] error:&err];
NSLog(#"[%#] %lu objects in table", [self class], (long)numOfRows);
return numOfRows;
}
/**
* Part of the NSTableViewDataSource protocol. This method must return the object for a specific cell,
* The cell is identified by a row number and a NSTableColumn object.
*/
-(id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
NSError * err;
NSArray* result = [managedObjContext executeFetchRequest:[self createSeasonFetchRequest] error:&err];
Season *season = [result objectAtIndex:row];
NSObject* obj = [season valueForKey:[tableColumn identifier]];
NSLog(#"[%#] Index: %lu - %#", [self class], (long)row, obj);
return obj;
}
/**
* Part of the NSTableViewDataSource protocol. This method sets the value for an entity, that was entered in the table.
* The value to insert is identified by a row number and a NSTableColumn object.
*/
- (void)tableView:(NSTableView *)tableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row {
NSError * err;
NSArray* result = [managedObjContext executeFetchRequest:[self createSeasonFetchRequest] error:&err];
Season *season = [result objectAtIndex:row];
[season setValue:object forKey:[tableColumn identifier]];
}
/**
* Creates a fetch request for the Season entity. The request does not include any subentities.
*/
-(NSFetchRequest*) createSeasonFetchRequest {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:[NSEntityDescription entityForName:#"Season" inManagedObjectContext:managedObjContext]];
[fetchRequest setIncludesSubentities:NO];
return fetchRequest;
}
/**
* Called when the 'Add Season' button is pressed in the gui. It creates a new (empty) Season object
* within the managedObjectConext and forces the season table to refresh;
*/
- (IBAction)addSeason:(id)sender {
NSLog(#"[%#] 'Add Season' button has been pressed...", [self class]);
[NSEntityDescription insertNewObjectForEntityForName:#"Season" inManagedObjectContext:managedObjContext];
[seasonTable reloadData];
}
#end
As Tom pointed out, I missed to add an NSSortDescriptor.
I modified my code and it works like charm.
/**
* Creates a fetch request for the Season entity. The request does not include any subentities.
*/
-(NSFetchRequest*) createSeasonFetchRequest {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:[NSEntityDescription entityForName:#"Season" inManagedObjectContext:managedObjContext]];
[fetchRequest setIncludesSubentities:NO];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
[fetchRequest setSortDescriptors:#[sortDescriptor]];
return fetchRequest;
}
I read this article to get a more clear understanding of how to use basic NSSort features: Apple Developer - CoreData Fetching
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];
}
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];
I've been searching, reading, trying different approaches, etc. for the last couple weeks trying to solve this issue, but no luck... Any help is appreciated.
I have an NSTableView which uses my controller class as it's datasource, an NSMutableArray(validPropertyList) of NSDictionaries to be more exact. I have the dictionaries holding one NSNumber numberWithBool with a key of #"checkboxValue", and one NSString with a key of #"address". I initially set all the checkboxValues to YES.
for (int i =0; i<properties.count; i++) {
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithObjectsAndKeys:[properties objectAtIndex:i],#"address",[NSNumber numberWithBool:YES],#"checkboxValue", nil];
[validPropertyList addObject:dict];
}
My table has 2 columns, whose identifiers correspond to the key names of my dictionaries.
In IB, I dragged over a Check Box Cell into the "checkboxValues" column. The required methods are below for the table.
-(NSInteger)numberOfRowsInTableView:(NSTableView *)tableView{
return [validPropertyList count];
}
-(id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row{
NSString *identifier = [tableColumn identifier];
return [[validPropertyList objectAtIndex:row]valueForKey:identifier];
}
All of that works fine. The table loads a (selected)checkbox in the first column and my string object in the other one for each dictionary in my array.
I implemented the below method in order to handle the state changes of the checkboxes in the table...
-(void)tableView:(NSTableView *)tableView setObjectValue:(id)object forTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row{
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithDictionary:[validPropertyList objectAtIndex:row]];
[dict setValue:object forKey:#"checkboxValue"];
[validPropertyList replaceObjectAtIndex:row withObject:dict];
// NSLog(#"val is now %#",[[validPropertyList objectAtIndex:row]valueForKey:#"checkboxValue"]);
}
This appears to work as well. Clicking on a checkbox visually changes it's state. It also updates the [NSNumber numberWithBool] value in my array as it should.
I have a checkbox outside of the table that I'm trying to use to either select all or deselect all of the checkboxes in the table. I have a bool variable(selectAllValue) in my class that is bound to the state of this checkbox, and this checkbox calls the action below...
- (IBAction)selectAll:(id)sender {
if (selectAllValue == YES) {
for (int i=0; i<[validPropertyList count]; i++) {
NSMutableDictionary *dict = [validPropertyList objectAtIndex:i];
[dict setValue:[NSNumber numberWithBool:YES] forKey:#"checkboxValue"];
[validPropertyList replaceObjectAtIndex:i withObject:dict];
}
}
else{
for (int i=0; i<[validPropertyList count]; i++) {
NSMutableDictionary *dict = [validPropertyList objectAtIndex:i];
[dict setValue:[NSNumber numberWithBool:NO] forKey:#"checkboxValue"];
[validPropertyList replaceObjectAtIndex:i withObject:dict];
NSLog(#"new value in array when should be no is: %#",[validPropertyList objectAtIndex:i]);
}
}
[tv reloadData];
}
This does set all of the values in my array to the correct values, and then I call the [tv reloadData] to update the table. It doesn't visually change the values of the checkboxes though. That's my problem.
I've tried adding checkboxes to my dictionary instead of NSNumbers, but no difference. I've tried binding the checkboxes in the table to the bool selectAllValue, but no difference.
I've read other posts on SO that relate to checkboxes in tables, but none of them really address this. Again, any help is greatly appreciated.
Please let me know if you need more information.
Thanks
Scott
I am a newbie. I have written some code for UITable but I am unable to update table values after adding the values to the array. I'm using a tableview subclass. The code is as follows. Check the last function. Now how can I update my table values?
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
//#warning Potentially incomplete method implementation.
// Return the number of sections.
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
//#warning Incomplete method implementation.
// Return the number of rows in the section.
return [_phones count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
}
HLPFoundPhones *p = (HLPFoundPhones *)[_phones objectAtIndex:indexPath.row];
NSString *subtitle = [NSString stringWithFormat:#"Found (%1.2f,%1.2f) on %#" ,[p loc].x, [p loc].y , [p foundDate]];
cell.textLabel.text = [p name];
cell.detailTextLabel.text = subtitle;
return cell;
}
- (void)insertNewObject
{
HLPPhonesAdd *view = [[HLPPhonesAdd alloc]initWithNibName:#"HLPPhonesAdd" bundle:nil];
[self.navigationController pushViewController:view animated:YES];
[view release];
}
- (void)updateTable:(CGPoint)loc name:(NSString *)_name{
HLPFoundPhones *a = [[HLPFoundPhones alloc]initWithLoc:loc name:_name];
[_phones addObject:a];
[phones reloadData];
}
call
[yourTableView reloadData];
in your updateTable Function..
- (void)updateTable:(CGPoint)loc name:(NSString *)_name{
HLPFoundPhones *a = [[HLPFoundPhones alloc]initWithLoc:loc name:_name];
[_phones addObject:a];
//give your tableView object instead of yourTableView
[yourTableView reloadData];
}
check value of _phone count and use in function
- (void)updateTable:(CGPoint)loc name:(NSString *)_name
{
HLPFoundPhones *a = [[HLPFoundPhones alloc]initWithLoc:loc name:_name];
[_phones addObject:a];
[table_name reloaddata];
}
Confirm the array _phones is allocated.
Then there will be problems if the [tableview reloadData] is called inside a thread.
So confirm the reloadData is called from the main thread.
First of all, your cell identifier looks to be same for all cells, which may confuse iOS which cell needs to be dequeued. So, keep your cell identifiers unique, something like below :
NSString *cellIdentifier = [NSString stringWithFormat:#"Cell-%d", indexPath.row];
then, after table view is first displayed, update your datasource and call:
[tableView reloadData];