views:

671

answers:

1

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: http://stackoverflow.com/questions/1112521/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 ?

A: 

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;
}
Alex Reynolds
Hi Alex,Thanks for your help. The above code does work. But the fetchedresultscontroller sections were not as expected. For instance, if there were two last name values reading 111 and 222, there were two sections in the table (one for 1 and the other for 2). To circumvent this problem and to group them under "#", I prepended # to last names starting with a number while storing in core data. Had to strip it if last name starts with # in the table renderer. I am sure this is not a neat way to do it though. Is there a better way of handling this with the fetched results controller ?
KSH
Additionally, the # section appears at the top above "A" (because of the sort order specified for the fetched results controller). But the default contacts app renders # at the bottom which is valid too. Is it going to be possible to achieve this with fetched results controller at all ? Or should we hand code the entire logic managing various arrays ? I prefer the fetched results controller for memory handling and ease of use.
KSH
Update: I added another numeric column to indicate whether the last name started with an alphabet or not and sorted on it first and then sorted again on the last name column. ie. two sort descriptors which yielded appropriate results (I also pushed # to the bottom of the index). Works fine so far but what if the app is localized in some other language ? How do I check if the given starting letter of the name is part of the standard charset and is available in the index (assuming the index is localized).
KSH