UITableView grouping sections - iphone

I have a UITableView that's populated using core data & sqlite.
I'd like to have sections grouped in the UITableView based on an attribute from the database table.
e.g If i had a category field in the database table called "type", what would be the best way of sectioning that data out?
I've seen examples using arrays, but I'm getting stuck with the core data. all the data is currently displayed from the database and I'd like to section it out somehow.
thanks in advance.

If you are using an NSFetchedResultsController to fetch your results and connect them to your UI it's pretty easy. Just set the sectionNameKeyPath: parameter of the initWithFetchRequest call to NSFetchedResultsController.
In this example, which is only slightly modified from the class reference for NSFetchedResultsController I have defined a key path that will use the section named "group" as the section title. Thus, if you have rows in your database that have a group set to "Cats" and other rows with a group set to "Dogs" your resulting table view will have 2 sections - one for cats and one for dogs.
NSManagedObjectContext *context = <#Managed object context#>;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Configure the request's entity, and optionally its predicate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"<#Sort key#>" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
[sortDescriptors release];
[sortDescriptor release];
NSFetchedResultsController *controller = [[NSFetchedResultsController alloc]
initWithFetchRequest:fetchRequest
managedObjectContext:context
sectionNameKeyPath:#"groups"
cacheName:#"<#Cache name#>"];
[fetchRequest release];
NSError *error;
BOOL success = [controller performFetch:&error];
For more information about key-paths you have to search for the key path documentation in the Xcode doc set. For simple cases though, it's just the name of an attribute of your returned objects.

I found an array to be really useful when using sections. Take a look at my example code
Sort friends (From CoreData)
NSMutableArray* unsortedFriends = [appDelegate.core.serviceManager.storageManager getFriendList];
for(ELMUser* user in unsortedFriends) {
if ([user.friendshipConfirmed boolValue]) {
if (![user.localDeleted boolValue]) {
[friendList addObject:user];
}
} else {
if (![user.localDeleted boolValue]) {
[friendListUnconfirmed addObject:user];
}
}
}
listOfItems = [[NSMutableArray alloc] init];
[listOfItems addObject:friendList];
[listOfItems addObject:friendListUnconfirmed];
Display Cell
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = #"Cell";
NSMutableArray *subArray = [listOfItems objectAtIndex:indexPath.section];
ELMUser* user = (ELMUser*)[subArray objectAtIndex:indexPath.row];
....

Related

Declared and synthesized NSArray works in all methods but one

I have an array of NSMutableDictionaries which has been sorted.
This array has been declared and synthesized so that its reachable anywhere in the code. However, its not.
When I try to read it out in cellForRowAtIndexPath, I get 0x5852400 does not appear to point to a valid object in debugger by using the po command, and I get the EXC_BAD_ACCESS error when using NSLog.
Code:
- (void)request:(FBRequest *)request didLoad:(NSArray*)result
{
int counter;
friendsArray = [[NSMutableArray alloc] init];
for (NSDictionary *d in [result objectForKey:#"data"])
{
[friendsArray addObject:d];
counter++;
}
NSSortDescriptor *sortDescriptor;
sortDescriptor = [[[NSSortDescriptor alloc] initWithKey:#"first_name" ascending:YES] autorelease];
NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
NSArray *sortedArray;
newfriendsArray = [[NSArray alloc] init];
newfriendsArray = [friendsArray sortedArrayUsingDescriptors:sortDescriptors];
NSLog(#"The new array which works and has been sorted: %#", newfriendsArray);
[[self tableView] reloadData];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
NSLog(#"Array still working here: %#", newfriendsArray);
return [newfriendsArray count];
}
Doing the same NSlog like the ones above in cellForRowAtIndexPath will cause the simulator to crash.
It's a memory management error. In this line:
newfriendsArray = [friendsArray sortedArrayUsingDescriptors:sortDescriptors];
sortedArrayUsingDescriptors: returns an autoreleased object that you need to retain if you want to use it for longer than the current method. The line above:
newfriendsArray = [[NSArray alloc] init];
has no effect other than introduce a memory leak to your code. It seems you have not quite understood when you need to create new objects and when other methods do this for you. In addition, you should really use properties to set your ivars (self.newFriendsArray = ...;) to avoid these simple memory management errors.

Core Data NSSet Issue

I auto-generated the cored data NSManagedObject class, and what I am trying to do is to display all phone numbers for a certain person. I am trying to display this in my table view class to populate the table.
Person to Numbers is a one-to-many relationship, and the numbers are in the set which I did by the addNumbersObject method. I just do not understand how to fetch this in the fetchresultscontroller and display them in the table view.
Currently I am just fetching all people.
Any ideas or suggestions?
Core Data Class:
#class Number
#interface Person : NSManagedObject
#property (nonatomic, retain) NSString *name;
#property (nonatomic, retain) NSSet *numbers;
#end
#interface Person (CoreDataGeneratedAccessors)
- (void)addNumbersObject:(Number *)value;
- (void)removeNumbersObject:(Number *)value;
- (void)addNumbers:(NSSet *)values;
- (void)removeNumbers:(NSSet *)values;
#end
Table View Class:
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil)
return _fetchedResultsController;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Person" inManagedObjectContext:_context];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:NO];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:_context sectionNameKeyPath:nil cacheName:#"Root"];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
[fetchRequest release];
[theFetchedResultsController release];
return _fetchedResultsController;
}
Try using a predicate on your fetchRequest.
I am not sure from your question if you want to display all the phone numbers for all the people or how you want that formatted, but this is how to get all the numbers for one person.
To get all the phone numbers for one specific person in one table view:
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Number" inManagedObjectContext:_context];
[fetchRequest setPredicate:[NSPredicate predicateWithFormat:#"person.name == %#", personName]]; //Assuming person.name is unique
Then sort that person's phone number like you want it.
There are a lot of ways you might "display all phone numbers for a person" -- think specifically about what you want your table to contain and how it should be organized, and you might have an easier time finding the answer you're looking for.
In the meantime, here's one approach: Use table sections to represent Persons, and rows within each for their associated Numbers. You can do this like so:
Set Number as the entity for your fetch request.
Set "person.name" as the sort key. (You might want to add a second sort descriptor so that Numbers for each person are in a sensible order.)
Also set "person.name" as the section name key path.
Implement your table view data source's -tableView:titleForHeaderInSection: thusly:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo name];
}

Dynamically setting fetchLimit for NSFetchedResultsController

I'm using am NSFetchedResultsController to populate data onto a UITableView.
It's a simple chat app and I want to load the latest 25 messages onto the table first and load more as the user scrolls up to see older messages (the chat message are in a ascending order).
I call a method that will setFetchLimit: for the NSFetchedResultsController in the willDisplayCell: like so....
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.row == 0)
{
[self performSelector:#selector(getMoreMessages) withObject:nil afterDelay:1.0];
}
}
when the first row of the UITableView has been displayed, getMoreMessages will try to reset the fetchLimit reload the UITableView like so.....
- (void)getMoreMessages
{
maxListItems += 25;
NSLog(#"set maxListItems: %d", maxListItems);
[self.resultsController.fetchRequest setFetchLimit:maxListItems];
[self._tableView reloadData];
}
However, it doesn't seem to be working, the table data will not change.
The initial NSFetchRequest is set like so...
NSFetchRequest *chatDataRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"ChatData" inManagedObjectContext:appDelegate.managedObjectContext];
[chatDataRequest setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(key != 0 OR messageNo != 0) and matchNo = %d", matchNo];
[chatDataRequest setPredicate:predicate];
NSSortDescriptor *sortDescripter1 = [[NSSortDescriptor alloc] initWithKey:#"status" ascending:YES];
NSSortDescriptor *sortDescripter2 = [[NSSortDescriptor alloc] initWithKey:#"messageNo" ascending:YES];
NSArray *sortDescripters = [[NSArray alloc] initWithObjects:sortDescripter1, sortDescripter2, nil];
[chatDataRequest setSortDescriptors:sortDescripters];
[sortDescripters release];
[sortDescripter1 release];
[sortDescripter2 release];
[chatDataRequest setFetchLimit:25];
NSFetchedResultsController *fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:chatDataRequest managedObjectContext:appDelegate.managedObjectContext sectionNameKeyPath:nil cacheName:[NSString stringWithFormat:#"%d_chat.cache", matchNumber]];
[chatDataRequest release];
fetchedResultsController.delegate = self;
NSError *error;
BOOL success = [fetchedResultsController performFetch:&error];
if(!success) NSLog(#"error: %#", error);
self.resultsController = fetchedResultsController;
And back to the question.
How can one dynamically change the fetchLimit for an NSFetchedResultsController?
Any hits would be awesome!
Thanks!
Instand using setFetchLimit, using setBatchSize, see below for detail answer.
The count of the fetchedObjects array might not what you want to do, since it does not update the changes from the persistent store. From NSFetchedResultsController documentation:
The results array only includes instances of the entity specified by the fetch request (fetchRequest) and that match its predicate. (If the fetch request has no predicate, then the results array includes all instances of the entity specified by the fetch request.)
The results array reflects the in-memory state of managed objects in the controller’s managed object context, not their state in the persistent store. The returned array does not, however, update as managed objects are inserted, modified, or deleted.
If you only want to fetch 20 objects, set the fetch limit of the NSFetchRequest. If you want only to keep 20 objects in memory, use setBatchSize of the NSFetchRequest object.
figured this one out.
looks like I have to run performFetch: after I change the fetchLimit. :D
[self.resultsController.fetchRequest setFetchLimit:maxListItems];
[self.resultsController performFetch:&error];

Core Data: Fetch all entities in a to-many-relationship of a particular object?

in my iPhone application I am using simple Core Data Model with two entities (Item and Property):
Item
name
properties
Property
name
value
item
Item has one attribute (name) and one one-to-many-relationship (properties). Its inverse relationship is item. Property has two attributes the according inverse relationship.
Now I want to show my data in table views on two levels. The first one lists all items; when one row is selected, a new UITableViewController is pushed onto my UINavigationController's stack. The new UITableView is supposed to show all properties (i.e. their names) of the selected item.
To achieve this, I use a NSFetchedResultsController stored in an instance variable. On the first level, everything works fine when setting up the NSFetchedResultsController like this:
-(NSFetchedResultsController *) fetchedResultsController {
if (fetchedResultsController) return fetchedResultsController;
// goal: tell the FRC to fetch all item objects.
NSFetchRequest *fetch = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Item" inManagedObjectContext:self.moContext];
[fetch setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
[fetch setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetch setFetchBatchSize:10];
NSFetchedResultsController *frController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetch managedObjectContext:self.moContext sectionNameKeyPath:nil cacheName:#"cache"];
self.fetchedResultsController = frController;
fetchedResultsController.delegate = self;
[sort release];
[frController release];
[fetch release];
return fetchedResultsController;
}
However, on the second-level UITableView, I seem to do something wrong. I implemented the fetchedresultsController in a similar way:
-(NSFetchedResultsController *) fetchedResultsController {
if (fetchedResultsController) return fetchedResultsController;
// goal: tell the FRC to fetch all property objects that belong to the previously selected item
NSFetchRequest *fetch = [[NSFetchRequest alloc] init];
// fetch all Property entities.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Property" inManagedObjectContext:self.moContext];
[fetch setEntity:entity];
// limit to those entities that belong to the particular item
NSPredicate *predicate = [NSPredicate predicateWithFormat:[NSString stringWithFormat:#"item.name like '%#'",self.item.name]];
[fetch setPredicate:predicate];
// sort it. Boring.
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
[fetch setSortDescriptors:[NSArray arrayWithObject:sort]];
NSError *error = nil;
NSLog(#"%d entities found.",[self.moContext countForFetchRequest:fetch error:&error]);
// logs "3 entities found."; I added those properties before. See below for my saving "problem".
if (error) NSLog("%#",error);
// no error, thus nothing logged.
[fetch setFetchBatchSize:20];
NSFetchedResultsController *frController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetch managedObjectContext:self.moContext sectionNameKeyPath:nil cacheName:#"cache"];
self.fetchedResultsController = frController;
fetchedResultsController.delegate = self;
[sort release];
[frController release];
[fetch release];
return fetchedResultsController;
}
Now it's getting weird. The above NSLog statement returns me the correct number of properties for the selected item. However, the UITableViewDelegate method tells me that there are no properties:
-(NSInteger) tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
NSLog(#"Found %d properties for item \"%#\". Should have found %d.",[sectionInfo numberOfObjects], self.item.name, [self.item.properties count]);
// logs "Found 0 properties for item "item". Should have found 3."
return [sectionInfo numberOfObjects];
}
The same implementation works fine on the first level.
It's getting even weirder. I implemented some kind of UI to add properties. I create a new Property instance via Property *p = [NSEntityDescription insertNewObjectForEntityForName:#"Property" inManagedObjectContext:self.moContext];, set up the relationships and call [self.moContext save:&error]. This seems to work, as error is still nil and the object gets saved (I can see the number of properties when logging the Item instance, see above). However, the delegate methods are not fired. This seems to me due to the possibly messed up fetchRequest(Controller).
Any ideas? Did I mess up the second fetch request? Is this the right way to fetch all entities in a to-many-relationship for a particular instance at all?
You need to actually perform the fetch for the table view controller:
// ...create the fetch results controller...
NSError *fetchRequestError;
BOOL success = [fetchedResultsController performFetch:&fetchRequestError];

NSFetchRequest / Predicate Question

I have two entities: Patient and Job. Patient has a to-many relationship to Job called 'jobs' and Job has a to-one relationship to Patient called 'patient'. Job has attributes called 'dueDate' (Date) and 'completed' (BOOL) and Patient has attributes 'firstName' and 'lastName' (both Strings).
I am trying to create a fetch request / predicate for my NSFetchedResultsController that we grab all Jobs that have not been completed (i.e. completed == NO) and section them by Patient name. Here is my code:
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Job" inManagedObjectContext:moc];
[fetchRequest setEntity:entity];
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(completed == NO)"];
[fetchRequest setPredicate:predicate];
NSSortDescriptor *patientDescriptor = [[NSSortDescriptor alloc] initWithKey:#"patient" ascending:YES];
NSSortDescriptor *dueDateDescriptor = [[NSSortDescriptor alloc] initWithKey:#"dueDate" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:patientDescriptor, dueDateDescriptor, nil];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:moc sectionNameKeyPath:#"patient" cacheName:#"Jobs"];
Here is my titleForHeaderInSection method:
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:section];
NSString *firstName = [[(Job *)[fetchedResultsController objectAtIndexPath:indexPath] patient] firstName];
NSString *lastName = [[(Job *)[fetchedResultsController objectAtIndexPath:indexPath] patient] lastName];
return [NSString stringWithFormat:#"%# %#", firstName, lastName];
This doesn't seem to work. Am I going about this the wrong way?
How is it not working? It helps to describe what results you are seeing.
You are not adding your sort descriptors to your NSFetchRequest, at least in the sample you provided.
Your sort descriptors are ineffective. It appears that Patient is a relationship so sorting against the relationship will not work. You would want to do a sort like the following:
NSSortDescriptor *lastNameDescriptor = [[NSSortDescriptor alloc] initWithKey:#"patient.lastName" ascending:YES];
NSSortDescriptor *firstNameDescriptor = [[NSSortDescriptor alloc] initWithKey:#"patient.firstName" ascending:YES];
NSSortDescriptor *dueDateDescriptor = [[NSSortDescriptor alloc] initWithKey:#"dueDate" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects: lastNameDescriptor, firstNameDescriptor, dueDateDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
[lastNameDescriptor release], lastNameDescriptor = nil;
[firstNameDescriptor release], firstNameDescriptor = nil;
[dueDateDescriptor release], dueDateDescriptor = nil;
You do not need to cast [fetchedResultsController objectAtIndexPath:indexPath] as it returns id.
What are you getting back from the call to [fetchedResultsController objectAtIndexPath:indexPath]? Put a breakpoint here and check the value and make sure you are getting back a NSManagedObject instead of nil. Putting a breakpoint in that method will also confirm that you are getting called.
Your secondKeypathName will not work as mentioned above. You probably want to set it to #"patient.lastName" so that it will match the initial sort I described above.
Your -tableView: titleForHeaderInSection: should be accessing the cache provided by the NSFetchedResultsController instead of assuming that there is going to be a row in the section:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
id sectionInfo = [[fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo name];
}
Finally, if you want the section to truly display the "lastname, firstname" format then you will need to create a non-transient derived value property on your Patient entity that is the fullName so that you can create your cache based on it. This derived value would then need to be updated any time that the first name or last name were changed.
First, you don't seem to attach the sortDescriptors to fetchRequests. This may or may not be connected with the problem.
Second, you can accomplish this in an easier way. Make it like this:
sectionNameKeyPath:#"patient.name"
"name" should be a property or method of the Patient object. An easy way to implement this would be to have a category method on patient:
- (NSString *)name {
return [NSString stringWithFormat:#"%# %#", self.firstName, self.lastName];
}
Actually, you can't accomplish your purpose with something as simple as this, read mzarra's answer for correct answer. NSFetchedResultsController has this critical comment:
If the controller generates sections, the first sort descriptor in the array is used to group the objects into sections; its key must either be the same as sectionNameKeyPath or the relative ordering using its key must match that using sectionNameKeyPath.
But, you cannot sort on the results of a method call, you'd need a property of the object. So, your best bet is probably just have a "name" property on "patient", and use that property for both sorting and sectionNameKeyPath.
In addition to not assigning your sortDescriptors to your fetchRequest, I believe you have a problem with your predicate. Since you are dealing with Core Data, the boolean value for your "completed" attribute is stored in an instance of NSNumber. Something like the predicate below would be better:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"completed = %#", [NSNumber numberWithBool:NO]];