My Requirement:
I have this straight forward requirement of listing names of people in alphabetical order in a Indexed table view with index titles being the starting letter of alphabets (additionally a search icon at the top and # to display misc values which start with a number and other special characters).
What I have done so far:
1. I am using core data for storage and "last_name" is modelled as a String property in the Contacts entity
2.I am using a NSFetchedResultsController to display the sorted indexed table view.
Issues accomplishing my requirement:
1. First up, I couldn't get the section index titles to be the first letter of alphabets. Dave's suggestion in the following post, helped me achieve the same: NSFetchedResultsController with sections created by first letter of a string
The only issue I encountered with Dave' suggestion is that I couldn't get the misc named grouped under "#" index.
What I have tried:
1. I tried adding a custom compare method to NSString (category) to check how the comparison and section is made but that custom method doesn't get called when specified in the NSSortDescriptor selector.
Here is some code:
@interface NSString (SortString)
-(NSComparisonResult) customCompare: (NSString*) aStirng;
@end
@implementation NSString (SortString)
-(NSComparisonResult) customCompare:(NSString *)aString
{
NSLog(@"Custom compare called to compare : %@ and %@",self,aString);
return [self caseInsensitiveCompare:aString];
}
@end
Code to fetch data:
NSArray *sortDescriptors = [NSArray arrayWithObject:[[[NSSortDescriptor alloc] initWithKey:@"last_name"
ascending:YES selector:@selector(customCompare:)] autorelease]];
[fetchRequest setSortDescriptors:sortDescriptors];
fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext sectionNameKeyPath:@"lastNameInitial" cacheName:@"MyCache"];
Can you let me know what I am missing and how the requirement can be accomplished ?
This is a really inefficient first-pass at this problem, which I am going to rewrite eventually. But hopefully this will help you.
The idea of this is to "guarantee" getting a real table section index back when tapping a "standard" section index view. A standard section index view should have a magnifying lens icon for search, a hash mark (#) for non-alphabetical sections, and letters A through Z for alphabetical sections.
This standard view is presented regardless of how many real sections there are, or what they are made of.
Ultimately, this code maps section view indices to real-existing alphabetic section name paths in the fetched results controller, or to real-existing non-alphabetic (numerical) sections, or to the search field in the table header.
The user will only occasionally recreate the section index mapping array (_idxArray
) on each touch of the section index, but recreating the array on each touch is obviously inefficient and could be tweaked to cache pre-calculated results.
There are a lot of places to start tightening this up: I could make the sectionIndexTitleLetters
static string all uppercase from the start, for example. It's fast enough on a 3GS phone, though, so I haven't revisited this recently.
In the header:
static NSString *sectionIndexTitleLetters = @"abcdefghijklmnopqrstuvwxyz";
In the implementation of the table view data source:
- (NSArray *) sectionIndexTitlesForTableView:(UITableView *)tv {
if (tv != searchDisplayController.searchResultsTableView) {
NSMutableArray *_indexArray = [NSMutableArray arrayWithCapacity:([sectionIndexTitleLetters length]+2)];
[_indexArray addObject:@"{search}"];
[_indexArray addObject:@"#"];
for (unsigned int _charIdx = 0; _charIdx < [sectionIndexTitleLetters length]; _charIdx++) {
char _indexChar[2] = { toupper([sectionIndexTitleLetters characterAtIndex:_charIdx]), '\0'};
[_indexArray addObject:[NSString stringWithCString:_indexChar encoding:NSUTF8StringEncoding]];
}
return _indexArray;
}
return nil;
}
- (NSInteger) tableView:(UITableView *)tv sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index {
if (tv != searchDisplayController.searchResultsTableView) {
if (index == 0) {
//
// This is the search bar "section"
//
[currentTableView scrollRectToVisible:[[currentTableView tableHeaderView] bounds] animated:YES];
return -1;
}
else if (index == 1) {
//
// This is the "#" section, which covers non-alphabetic section headers (e.g. digits 0-9)
//
return 0;
}
else {
//
// This is a bit more involved because the section index array may contain indices that do not exist in the
// fetched results controller's sections->name info.
//
// What we are doing here is building a "fake-index" array that will return a real section index regardless of
// whether the section index title being touched exists or not.
//
// The fake array will be of length of the section index title array, and each index will contain an unsigned
// integer from 1 to {numOfRealSections}.
//
// The value this array returns will be "nearest" to the real section that is in the fetched results controller.
//
NSUInteger _alphabeticIndex = index-2;
unsigned int _idxArray[26];
for (unsigned int _initIdx = 0; _initIdx < [sectionIndexTitleLetters length]; _initIdx++) {
_idxArray[_initIdx] = [[fetchedResultsController sections] count] - 1;
}
unsigned int _previousChunkIdx = 0;
NSNumberFormatter *_numberFormatter = [[NSNumberFormatter alloc] init];
NSLocale *_enUSLocale = [[NSLocale alloc] initWithLocaleIdentifier: @"en_US"];
[_numberFormatter setLocale:_enUSLocale];
[_enUSLocale release];
for (unsigned int _sectionIdx = 0; _sectionIdx < [[fetchedResultsController sections] count]; _sectionIdx++) {
NSString *_sectionTitle = [[[fetchedResultsController sections] objectAtIndex:_sectionIdx] name];
if (![_numberFormatter numberFromString:_sectionTitle]) {
// what's the index of the _sectionTitle across sectionIndexTitleLetters?
for (unsigned int _titleCharIdx = 0; _titleCharIdx < [sectionIndexTitleLetters length]; _titleCharIdx++) {
NSString *_titleCharStr = [[sectionIndexTitleLetters substringWithRange:NSMakeRange(_titleCharIdx, 1)] uppercaseString];
if ([_titleCharStr isEqualToString:_sectionTitle]) {
// put a chunk of _sectionIdx into _idxArray
unsigned int _currentChunkIdx;
for (_currentChunkIdx = _previousChunkIdx; _currentChunkIdx < _titleCharIdx; _currentChunkIdx++) {
_idxArray[_currentChunkIdx] = _sectionIdx - 1;
}
_previousChunkIdx = _currentChunkIdx;
break;
}
}
}
}
[_numberFormatter release];
return (NSInteger)_idxArray[_alphabeticIndex];
}
}
return 0;
}
I might be naive, but I don't understand why these solutions are so baroque. I did this:
In my model, I added a method:
-(NSString *)lastInitial {
return [self.lastname substringToIndex:1];
}
And in my tablecontroller I set the fetchedresultscontroller to use that method:
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:@"lastInitial" cacheName:@"Master"];
Seems to work - is there a reason that this is a bad idea? Or am I benefitting from new features in ios5 or something?