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
Related
I have an app that stores information for golf rounds. I have three Entities that I am persisting data to: Rounds, Courses, and Tees. I structured the schema in this way because there is a two-many relationship between Rounds and Courses, and a Course can have multiple Tees (blue, white, gold, etc.)
I have a UITableview that I would like to display the results of each round in. However, I would like to display data from the Rounds Entity as well as the Courses Entity. Ideally I would also like to display the Tee for that round as well,but it's not a priority.
My question is, how do I use a FetchResultsController to get data from the three entities and display it in a single cell of a UITableview?
Here is how I am saving the data to the Entities:
HandicapAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext* context = [appDelegate managedObjectContext];
NSEntityDescription *roundsEntity = [NSEntityDescription entityForName:#"Rounds" inManagedObjectContext:context];
NSFetchRequest *request =[[NSFetchRequest alloc]init];
[request setEntity:roundsEntity];
Rounds * rounds = [NSEntityDescription insertNewObjectForEntityForName:#"Rounds" inManagedObjectContext:context];
[rounds setValue:score forKey:#"roundScore"];
[rounds setValue:date forKey:#"roundDate"];
[rounds setValue:differential forKey:#"roundDifferential"];
Courses * courses = [NSEntityDescription insertNewObjectForEntityForName:#"Courses" inManagedObjectContext:context];
[courses setValue:rating forKey:#"courseRating"];
[courses setValue:slope forKey:#"courseSlope"];
[courses setValue:courseName forKey:#"courseName"];
rounds.courses = courses;
Tee * tee = [NSEntityDescription insertNewObjectForEntityForName:#"Tee" inManagedObjectContext:context];
[tee setValue:teeColor forKey:#"teeColor"];
courses.tees = tee;
NSError *error;
[context save:&error];
And then this is my FetchedResultsController
if (_fetchedResultsController != nil)
{
return _fetchedResultsController;
}
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
fetchRequest.entity = [NSEntityDescription entityForName:#"Rounds" inManagedObjectContext:self.managedObjectContext];
// Set the batch size
[fetchRequest setFetchBatchSize:20];
// Set the sort descriptor
NSSortDescriptor * date = [[NSSortDescriptor alloc] initWithKey: #"roundDate"
ascending: NO];
NSArray * sortDescriptors = [NSArray arrayWithObjects: date, nil];
fetchRequest.sortDescriptors = sortDescriptors;
// Initialize fetched results controller - creates cache
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest: fetchRequest
managedObjectContext: self.managedObjectContext
sectionNameKeyPath: nil
cacheName: #"Master"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
// handle errors
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _fetchedResultsController;
How do I fetch the course name and tee (this will be the tee color) for the round in the tableview cell?
Thanks!!
Just follow the relationships. If you've got a one-to-many relationship between rounds and courses, you'd need to have a relationship on courses like rounds and the inverse relationship on rounds like course. So, if you've generated subclasses for your entities, you'd access the course name like this:
NSString *courseName = round.course.name;
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];
}
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).
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 have a view controller that is a subclass of UITableViewController. Here is my viewWillAppear:animated method:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if (fetchedResultsController != nil) {
[fetchedResultsController release];
fetchedResultsController = nil;
}
[self.fetchedResultsController performFetch:nil];
[self.tableView reloadData];
}
I am getting confused by seeing the fetchedResultsController being accessed when [super viewWillAppear:animated] is called. Since super is a UITableViewController, and there is no viewWillAppear:animated method for that class, per se, then its superclass viewWillAppear:animated should be called, right? If that's correct, then the UIViewController class should not be accessing UITableViewController delegate methods. But I see that numberOfSectionsInTableView is getting called. I'm not sure why the call to super viewWillAppear:animated would do this.
So before I explicitly run the peformFetch and reloadData, the table is getting populated. At that time, the data it is being populated with is out of date.
Here is the fetchedResultsController code
- (NSFetchedResultsController *) fetchedResultsController {
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
NSFetchRequest *fetchRequest = ...
NSEntityDescription * entity = ...
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:10];
NSSortDescriptor *aSortDescriptor = ...
NSSortDescriptor *bSortDescriptor = ...
NSArray *sortDescriptors = ...
[fetchRequest setSortDescriptors:sortDescriptors];
NSFetchedResultsController *aFetchedResultsController = ...
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
[aFetchedResultsController release];
[fetchRequest release];
...
[sortDescriptors release];
NSError *error = nil;
if (![fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved Error %#, %#", error, [error userInfo]);
abort();
}
return fetchedResultsController;
}
The documentation specifically describes this behaviour:
When the table view is about to appear the first time it’s loaded, the table-view controller reloads the table view’s data. It also clears its selection (with or without animation, depending on the request) every time the table view is displayed. The UITableViewController class implements this in the superclass method viewWillAppear:. You can disable this behavior by changing the value in the clearsSelectionOnViewWillAppear property.