I have a UITableView that is populated with data from core data and I currently have it set up so that it adjusts the size of the table based on the number of objects in an entity. My problem is that I know have two attributes in this entity. Now the tableview has a bunch of extra cells because it is creating one for everything in the entity, not just creating one per object in a specific attribute. So I would like to set numberOfRowsInSection: based on amount of objects in an attribute, not the whole entity. How would I do this?
How I am currently doing it:
id sectionInfo =
[[_fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
EDIT:
This is what I have tried:
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
// id sectionInfo =
// [[_fetchedResultsController sections] objectAtIndex:section];
// return [sectionInfo numberOfObjects];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSManagedObjectContext *context = [self managedObjectContext];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"FeedEntity" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"imageData != nill"];
NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:nil cacheName:nil];
id sectionInfo =
[[theFetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
But this results in this error:
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'An instance of NSFetchedResultsController requires a fetch request with sort descriptors'
Ok, well thats an easy solve. Will there always be an image for every single string? Why not just return the yourStringDataArray.length in the -(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section method? This way you will only get the amount of cells needed to display your total amount of strings in your array.
so you have one entity with two attributes. And you need to filter the objects to show for the tableview. I think you need to use NSFetchRequest and NSPredicate to filter your objects by checking the attribute of your object.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSManagedObjectContext *context = [your context];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"FeedEntity" inManagedObjectContext:context];
[fetchRequest setEntity:entity];
fetchRequest.predicate = [NSPredicate predicateWithFormat:#"imageData = nil"];
request.sortDescriptors = [NSArray arrayWithObject:[[NSSortDescriptor alloc] initWithKey:#"text" ascending:NO]];
NSFetchedResultsController *fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:context sectionNameKeyPath:nil cacheName:nil];
Related
This is my first attempt on Core Data so I would like your guidance.
My example project is quite simple, I would like to create an iOS app that displays a list of people. What I am looking for is to group all members of the same family together. Some people in the list do not belong to a "family". So, UITableView is going to be a mixture of groups and rows.
Here is my Model so far.
and here I am adding data in the context
Family *newFamily = [NSEntityDescription insertNewObjectForEntityForName:#“Family” inManagedObjectContext:self.managedObjectContext];
[newFamily setSurname:[dict objectForKey:#“surname”]];
NSMutableSet *members = [newTeam mutableSetValueForKey:#"members"];
for (NSDictionary *member in [dict objectForKey:#"members"]) {
Member *newMember = [NSEntityDescription insertNewObjectForEntityForName:#"Member" inManagedObjectContext:self.managedObjectContext];
[newMember setFirstName:[member objectForKey:#"firstName"]];
[newMember setAge:[member objectForKey:#“age”]];
[members addObject:newMember];
}
//Save context ...
In UITableViewController I am able to read and display just families'/groups' name. I am confused on how do you display all the objects of a section.
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Family" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"surname" ascending:NO];
[fetchRequest setSortDescriptors:#[sortDescriptor]];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"surname" cacheName:#"Master"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
return _fetchedResultsController;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return [[self.fetchedResultsController sections] count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
// I DON'T KNOW WHAT TO DO HERE ...
// return 0;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
[self configureCell:cell atIndexPath:indexPath];
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [self.fetchedResultsController sections][section];
return [sectionInfo name];
}
First, you need to amend your model to include the inverse relationship, from Member to Family, which presumably is "to-one". Name that relationship "family". (This inverse relationship is needed in this case, see below, but you should almost always include inverses for all relationships, regardless of whether you think you need them.)
Next, amend your FRC configuration as follows: the underlying entity should be Member, since each row of your tableView will represent a Member object. The sectionNameKeyPath should be "family.surname" so the FRC will automatically put the Member objects into sections based on the surname attribute of the related Family object. It is important that the sort order for the FRC matches up with the sections (ie all Member objects in the same section must be sorted together), so amend the sort descriptors to use family.surname (you could then sort by firstName if desired):
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Member" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Edit the sort key as appropriate.
NSSortDescriptor *surnameSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"family.surname" ascending:NO];
NSSortDescriptor *firstNameSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"firstName" ascending:NO];
[fetchRequest setSortDescriptors:#[surnameSortDescriptor, firstNameSortDescriptor]];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"family.surname" cacheName:#"Master"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
return _fetchedResultsController;
}
Finally, there is boilerplate code for linking an FRC to a tableView (see the Apple Docs here). The piece you are missing is:
- (NSInteger)tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section {
if ([[self.fetchedResultsController sections] count] > 0) {
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
} else
return 0;
}
Your configureCell method will likewise need to determine the correct Member object to display using
Member *currentMember = [self.fetchedResultsController objectAtIndexPath:indexPath];
Note that the FRC will log a warning message if there are Member objects that are not in a Family, since the sectionName will be nil. But it will group these all together - you might want to amend the titleForHeaderInSection to use a different name for that section.
Also, note that the FRC's sections are determined by inspecting the family.surname for each Member object. If there are Family objects that have no members, those Family objects will not appear in the table view (i.e. no empty sections).
I am using NSFetchedResultsController to load some data into UITableView. No matter how many objects I save, it only loads the first one. If I delete that one object from the UITableView and reload it, sometimes it will show one or more of the other objects I've saved. It's very unpredictable.
The strange thing is the code I was using worked fine on the iOS6 SDK. I know that the issue is with the NSFetchedResultsController because when I make a fetch request with NSFetchRequest, the data that is returned is correct. Here is the code;
- (void) viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSError *error;
if (![[self fetchedResultsController] performFetch:&error])
{
exit(-1);
}
}
- (void)viewDidUnload
{
self.fetchedResultsController = nil;
}
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil)
{
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"HatInfo" inManagedObjectContext:[self.appDelegate managedObjectContext]];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"lastSaved" ascending:NO];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:9];
NSArray *fetchedObjects = [[self.appDelegate managedObjectContext] executeFetchRequest:fetchRequest error:nil];
//returns the correct amount of objects
NSLog(#"FetchedObjects: %lu", (unsigned long)[fetchedObjects count]);
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:[self.appDelegate managedObjectContext] sectionNameKeyPath:nil
cacheName:#"Root"];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
- (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id sectionInfo = [[_fetchedResultsController sections] objectAtIndex:section];
//returns the incorrect amount
return [sectionInfo numberOfObjects];
}
This kind of unexpected behavior can happen if you used the same cacheName in other part of core data code (for example: another fetchedResultsController in your app.
You can try to change the cache name to something different or nil and see what happens.
Add this line before you start fetch :
[NSFetchedResultsController deleteCacheWithName:<< catch name>>];
I have a main tableViewController, touch a button self.navigation will push addItem viewController, click save self.navigation with pop the add VC, then we're back on the main one, I successfully add and save, also fetch, but when it comes to fetching number of cells, number returns 0, heres the method
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> secInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
NSLog(#"No. of cells determined: %lu", (unsigned long)[secInfo numberOfObjects]);
return[secInfo numberOfObjects];
}
NSLog gives me 0, whats the problem, this gave me headache.
Here's the fetch request:
- (NSFetchedResultsController*) fetchedResultsController {
// Fetch Request
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Goal"
inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"title"
ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// setting _fethcedResultsController
_fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
// setting _fetchedResultsController to self
_fetchedResultsController.delegate = self; // for the tableview updating thing
// Thats it
return _fetchedResultsController;
}
Please note that when I run a check for items, it's not nil:
// fetching all items to check how many of them exists
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity: [NSEntityDescription entityForName:#"Goal"
inManagedObjectContext:self.managedObjectContext]];
NSManagedObjectContext *moc = self.managedObjectContext;
NSError *fetchError = nil;
NSArray *goals = [moc executeFetchRequest:fetchRequest error:&fetchError];
NSLog(#"No. of goals: %lu", (unsigned long)[goals count]);
// end of check for items
I forgot to put this code of line:
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
in method
- (NSFetchedResultsController*) fetchedResultsController;
so before I did, the app always creates new fetchedResultsController and over-write the old one.
I am new at core data and am trying to figure out how to create a custom sectionNameKeyPath in my NSFetchedResultsController. I have a managed object with an attribute called acctPeriod. It is a NSString. I want to create sections based on the first 4 characters of this field. The first 4 characters represent the year of the accounting period and doesn't need to be saved.
I have gone through this site and have seen posts about transient attributes but I can't seem to get them to work. Basically I want this and then assign periodYear for my sectionNameKeyPath.
#dynamic periodYear;
-(NSString *)periodYear
{
return [self.acctPeriod substringToIndex:4];
}
Any help would be appreciated.
**UPDATE:
Using Martin R. answer, I was able to get it to work as expected.
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Billing" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"acctPeriod" ascending:NO];
NSArray *sortDescriptors = #[sortDescriptor];
//Predicate
NSPredicate *pred = [NSPredicate predicateWithFormat:#"clients = %#", self.client];
NSLog(#"%#",pred);
//[fetchRequest setResultType:NSDictionaryResultType];
//[fetchRequest setReturnsDistinctResults:YES];
[fetchRequest setPredicate:pred];
[fetchRequest setSortDescriptors:sortDescriptors];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"periodYear" cacheName:nil];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error])
{
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _fetchedResultsController;
}
The following should work: Implement the periodYear method (which will be used
as "section name key path") in a class extension of your managed object subclass:
#interface Event (AdditionalMethods)
- (NSString *)periodYear;
#end
#implementation Event (AdditionalMethods)
- (NSString *)periodYear {
return [self.acctPeriod substringToIndex:4];
}
#end
Make sure that acctPeriod is used as the first (or only) sort descriptor for the fetch request:
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"acctPeriod" ascending:YES];
NSArray *sortDescriptors = #[sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
Use periodYear as sectionNameKeyPath for the fetched results controller:
NSFetchedResultsController *_fetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:#"periodYear"
cacheName:nil];
_fetchedResultsController.delegate = self;
self.fetchedResultsController = _fetchedResultsController;
And finally add the default titleForHeaderInSection method:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo name];
}
Alternatively, you can define periodYear as transient attribute of the managed object.
It will also not be stored in the database in that case, but can be implemented in a way that the value is calculated on demand and cached.
The DateSectionTitles sample project from the Apple Developer Library demonstrates how this works.
I recommend against using a transient property as the sectionNameKeyPath as it will result in faulting all objects obtained by the fetch request just so the property could be used as the section path.
You better define a persistent property of year and use it as your sectionNameKeyPath.
set you FRC sectionNameKeyPath to year like so:
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:#"year"
cacheName:nil/*your chioce*/];
to display the section name as a title in the table, implement:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
id<NSFetchedResultsSectionInfo> sec = [self.fetchedResultsController sections][section];
return [sec name];
}
So I am trying to get a UITableView to show a list of objects by section (thing.title), but list them in descending order by date.
The table is is split into sections, which are labeled correctly (section headers are the different thing titles).
But the objects in each section are only half correct. The objects in each section are listed in descending order, but some sections contain data that should be in other sections.
An example of what is happening:
<Header> Big Title Name
<data><Big Title><id=1></data>
<data><Big Title><id=4></data>
**<data><Small Title><id=6></data>** <-- should not be in this section
<Header> Small Title Name
<data><Small Title><id=11></data>
<data><Big Title><id=23></data> <-- should not be in this section
**<data><Small Title><id=66></data>**
Here is part of my code:
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
/*
Set up the fetched results controller.
*/
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"AReads" inManagedObjectContext:[NSManagedObjectContext defaultContext]];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Sort using the timeStamp property..
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"timeStamp" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// Use the sectionIdentifier property to group into sections.
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:[NSManagedObjectContext defaultContext] sectionNameKeyPath:#"sessionTitle" cacheName:#"Root"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
return fetchedResultsController;
}
- (NSString*) tableView:(UITableView *)tableView titleForHeaderInSection: (NSInteger)section
{
//return [(Sessions*)[masterSessions objectAtIndex: section] title];
id <NSFetchedResultsSectionInfo> theSection = [[fetchedResultsController sections] objectAtIndex:section];
NSString *theTitle = [theSection name];
return theTitle;
}
The key used as sectionNameKeyPath of a fetched results controller and the key used in the first sort descriptor must either be the same keys or generate the same relative ordering. So you cannot use sessionTitle as sectionNameKeyPath and a completely different key timeStamp as sort descriptor for the sections.
In your case, it is probably the best to use timeStamp as sectionNameKeyPath and in the sort descriptor. This will ensure that all entries are correctly grouped into sections.
To display the sessionTitle instead of the timeStamp in the section header, you can modify titleForHeaderInSection:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.controller sections] objectAtIndex:section];
return [[[sectionInfo objects] objectAtIndex:0] sessionTitle];
}