I'm using Core Data for a table view, and I'd like to use the first letter of each of my results as the section header (so I can get the section index on the side). Is there a way to do this with the key path? Something like below, where I use name.firstLetter as the sectionNameKeyPath (unfortunately that doesn't work).
Do I have to grab the first letter of each result manually and create my sections like that? Is it better to put in a new property to just hold the first letter and use that as the sectionNameKeyPath?
NSFetchedResultsController *aFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext
sectionNameKeyPath:#"name.firstLetter"
cacheName:#"Root"];
Thanks.
**EDIT: ** I'm not sure if it makes a difference, but my results are Japanese, sorted by Katakana. I want to use these Katakana as the section index.
You should just pass "name" as the sectionNameKeyPath. See this answer to the question "Core Data backed UITableView with indexing".
UPDATE
That solution only works if you only care about having the fast index title scroller. In that case, you would NOT display the section headers. See below for sample code.
Otherwise, I agree with refulgentis that a transient property is the best solution. Also, when creating the NSFetchedResultsController, the sectionNameKeyPath has this limitation:
If this key path is not the same as
that specified by the first sort
descriptor in fetchRequest, they must
generate the same relative orderings.
For example, the first sort descriptor
in fetchRequest might specify the key
for a persistent property;
sectionNameKeyPath might specify a key
for a transient property derived from
the persistent property.
Boilerplate UITableViewDataSource implementations using NSFetchedResultsController:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return [[fetchedResultsController sections] count];
}
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
return [fetchedResultsController sectionIndexTitles];
}
- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index {
return [fetchedResultsController sectionForSectionIndexTitle:title atIndex:index];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
// Don't implement this since each "name" is its own section:
//- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
// id <NSFetchedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections] objectAtIndex:section];
// return [sectionInfo name];
//}
UPDATE 2
For the new 'uppercaseFirstLetterOfName' transient property, add a new string attribute to the applicable entity in the model and check the "transient" box.
There are a few ways to implement the getter. If you are generating/creating subclasses, then you can add it in the subclass's implementation (.m) file.
Otherwise, you can create a category on NSManagedObject (I put this right at the top of my view controller's implementation file, but you can split it between a proper header and implementation file of its own):
#interface NSManagedObject (FirstLetter)
- (NSString *)uppercaseFirstLetterOfName;
#end
#implementation NSManagedObject (FirstLetter)
- (NSString *)uppercaseFirstLetterOfName {
[self willAccessValueForKey:#"uppercaseFirstLetterOfName"];
NSString *aString = [[self valueForKey:#"name"] uppercaseString];
// support UTF-16:
NSString *stringToReturn = [aString substringWithRange:[aString rangeOfComposedCharacterSequenceAtIndex:0]];
// OR no UTF-16 support:
//NSString *stringToReturn = [aString substringToIndex:1];
[self didAccessValueForKey:#"uppercaseFirstLetterOfName"];
return stringToReturn;
}
#end
Also, in this version, don't forget to pass 'uppercaseFirstLetterOfName' as the sectionNameKeyPath:
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext
sectionNameKeyPath:#"uppercaseFirstLetterOfName" // this key defines the sections
cacheName:#"Root"];
And, to uncomment tableView:titleForHeaderInSection: in the UITableViewDataSource implementation:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo name];
}
There may be a more elegant way to do this, but I recently had the same problem and came up with this solution.
First, I defined a transient property on the objects I was indexing called firstLetterOfName, and wrote the getter into the .m file for the object.
e.x.
- (NSString *)uppercaseFirstLetterOfName {
[self willAccessValueForKey:#"uppercaseFirstLetterOfName"];
NSString *stringToReturn = [[self.name uppercaseString] substringToIndex:1];
[self didAccessValueForKey:#"uppercaseFirstLetterOfName"];
return stringToReturn;
}
Next, I set up my fetch request/entities to use this property.
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Object" inManagedObjectContext:dataContext];
[request setEntity:entity];
[NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES selector:#selector(caseInsensitiveCompare:)];
NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
[request setSortDescriptors:sortDescriptors];
Side note, apropos of nothing: Be careful with NSFetchedResultsController — it's not exactly fully baked yet IMHO, and any situation beyond the simple cases listed in the documentation, you will probably be better off doing it the 'old fashioned' way.
I solved this using the UILocalizedIndexCollation as mentioned in the NSFetchedResultsController v.s. UILocalizedIndexedCollation question
The elegant way is to do make the "firstLetter" a transient property, HOWEVER in practice that is slow.
It is slow because for a transient property to be calculated, the entire object needs to be faulted into memory. If you have a lot of records, it will be very, very slow.
The fast, but inelegant way, is to create a non-transient "firstLetter" property which you update each time you set your "name" property. Several ways to do this: override the "setName:" assessor, override "willSave", KVO.
See my answer to a similar question here, in which I describe how to create localized sectionKey indexes that are persisted (because you cannot sort on transient attributes in an NSFetchedResultsController).
Here is the simple solution for obj-c, several years and one language late . It works and works quickly in a project of mine.
First I created a category on NSString, naming the file NSString+Indexing. I wrote a method that returns the first letter of a string
#implementation NSString (Indexing)
- (NSString *)stringGroupByFirstInitial {
if (!self.length || self.length == 1)
return self;
return [self substringToIndex:1];
}
Then I used that method in the definition of the fetched result controller as follows
_fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"name.stringGroupByFirstInitial"
cacheName:nil];
The above code in conjunction with the two stringIndex methods will work with no need for messing about with transient properties or storing a separate attribute that just holds the first letter in core data
However when I try to do the same with Swift it throws an exception as it doesn't like having a function as part key path (or a calculated string property - I tried that too) If anyone out there knows how to achieve the same thing in Swift I would dearly like to know how.
Related
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];
}
I used iphones address book data in my app ,i sorted that data and arranged in UITableViewController,but now i want to make same look as of address book means sorted in GroupedStyle having initial character as heading of section .and suggestion plz
You have to implement the tableView methods that are
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
for section headers and for this this you also have to implement
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
By doing this your table data can be viewed in grouped style.
Use something like following to sort the contacts:
NSArray* tempArray = [jsonData objectForKey:#"contacts"];
NSSortDescriptor *sortDescriptor;
sortDescriptor = [[[NSSortDescriptor alloc] initWithKey:#"first_name"
ascending:YES] autorelease];
NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
contactsArray = [[NSArray alloc] initWithArray:[tempArray sortedArrayUsingDescriptors:sortDescriptors]];
From the use NSPredicate to pick the contacts and add them to section that you want.
I have a very basic CoreData backed iPhone application. After I forced the app to generate the sqlite file, I took it and prepopulated it with one record to test loading it into tableview.
I've hit a snag, though, because CoreData doesn't seem to be finding the row in the table.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return [fetchedResultsController sections] count];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
id <NSFetchedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo numberOfObjects];
}
The first function always returns one and the second function always returns zero. Since the tableview thinks there are no rows, it never hits cellForRowAtIndexPath, so none of my data is loaded.
I can, however, see my table structure in viewDidLoad with the following code:
if(!managedObjectContext){
managedObjectContext = [(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
}
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Assessment" inManagedObjectContext:managedObjectContext];
[request setEntity:entity];
for (NSPropertyDescription *property in entity)
{
NSLog(#"%#", property.name);
}
NSError *error = nil;
NSMutableArray *mutableFetchResults = [[managedObjectContext executeFetchRequest:request error:&error] mutableCopy];
[request release];
Now, it strikes me that nothing in the rest of my code ever generates an NSFetchRequest because I never hit cellForRowAtIndex. But I also based most of this code on the Recipe example, and it looks like it loads in the same way (and it actually works).
I'm sure I'm missing something obvious here, can someone point me in the right direction?
The code can be found in it's entirety here.
The problem is almost certainly in the setup of the fetched results controller e.g. wrong entity, wrong predicate etc.
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];
....
I am trying to implement a Core Data backed UITableView that supports indexing (eg: the characters that appear down the side, and the section headers that go with them). I have no problems at all implementing this without Core Data using:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section;
- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView;
I also have no problem implementing a UITableView that is backed by Core Data without using the indexing.
What I am trying to figure out is how to elegantly combine the two? Obviously once you index and re-section content, you can no longer use the standard NSFetchedResultsController to retrieve things at a given index path. So I am storing my index letters in an NSArray and my indexed content in an NSDictionary. This all works fine for display, but I have some real headaches when it comes to adding and deleting rows, specifically how to properly implement these methods:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller;
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath;
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type;
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller;
Because the index paths it's returning me have no correlation with the ones in core data. I got add working by simply rebuilding my index NSArray and NSDictionary when the user adds a row, but doing the same when they delete one crashes the whole application.
Is there a simple pattern/example I'm missing here to make all this work properly?
Edit: Just to clarify I know that the NSFetchedResultsController does this out of the box, but what I want is to replicate the functionality like the Contacts app, where the index is the first letter of the first name of the person.
You should use your CoreData NSFetchedResultsController to get your sections/indexes.
You can specify the section key in the fetch request (I think it has to match the first sort key):
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc]
initWithKey:#"name" // this key defines the sort
ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext
sectionNameKeyPath:#"name" // this key defines the sections
cacheName:#"Root"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
Then, you can get the section names like this:
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections] objectAtIndex:section];
return [sectionInfo name];
}
And the section indexes are here:
id <NSFetchedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections] objectAtIndex:section];
[sectionInfo indexTitle]; // this is the index
Changes to the content just indicate that the table needs to be updated:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView reloadData];
}
UPDATE
This only works for the index and fast index scrolling, not for the section headers.
See this answer to "How to use the first character as a section name" for more information and details on how to implement first letters for section headers as well as the index.