Benedict's Soapbox

How to use UILocalizedIndexedCollation to add a localized index to a UITableView (aka adding an A-Z index to a UITableView)

One of the lesser used features of UITableView is the section index. A section index is a list that flows down the side of the table and allows users to quickly move around the data. Contacts uses an index:

Contacts in English

To implement an index the table views data source must implement two methods:

sectionIndexTitlesForTableView: supplies an array of strings which are displayed as the index. In the case of the above screen shot, this would be an array of A to Z, with ‘search’ prepended and ‘#’ appended. (The table view substitutes the string {search} with the search icon.)

– tableView:sectionForSectionIndexTitle:atIndex: is used to tell the table view which section to jump to when an index is selected. This method exists to allow complicated mappings between a index and a section. For example, imagine that the index was configured like the screen shot above but the table only contains the following data:

Section Value
B Brown, James
D Dylan, Bob
Z Zappa, Frank

The index contains 28 items, but the table only contains 3 sections. tableView:sectionForSectionIndexTitle:atIndex: allows the data source to map the values to the index to a table section. The following table shows what tableView:sectionForSectionIndexTitle:atIndex: should return given the above data (let’s ignore search for the time being):

Section to jump to

Selected index Section to jump to
A B
B B
C B
D D
E-Y D
Z Z
# Z

That’s the basics of a table view index. It wouldn’t be too tricky to implement sectionIndexTitlesForTableView: and tableView:sectionForSectionIndexTitle:atIndex: for the above data. However, things get a lot more complicated when we take into account languages other than English. Here’s Contacts in English and Swedish:

Contacts in English Contacts in Swedish

There are a few things that these screen shots illustrate:

Just taking these differences into account would take a fair while, but imagine having to do that for all 34 languages supported by iOS. That would be painful. Thankfully these problems are solved by UILocalizedIndexedCollation. UILocalizedIndexedCollation performs 3 tasks:

  1. Supply a localized table index
  2. Partition the data into the correct section
  3. Determine the correct section to go to for a given index

Here’s how to implement it:

1. Supply a localized table index


- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView  
{  
    return [[UILocalizedIndexedCollation currentCollation] sectionIndexTitles];  
}  

2. Partition the data into the correct section


//@property(readwrite, copy, nonatomic) NSArray *tableData;

-(void)viewDidLoad  
{  
    NSArray *objects = aMagicalSourceOfWonderfullyInterestingObjects;  
    self.tableData = [self partitionObjects:objects collationStringSelector:@selector(title)];  
}

-(NSArray *)partitionObjects:(NSArray *)array collationStringSelector:(SEL)selector
{  
    UILocalizedIndexedCollation *collation = [UILocalizedIndexedCollation currentCollation];
    NSInteger sectionCount = [[collation sectionTitles] count]; //section count is take from sectionTitles and not sectionIndexTitles  
    NSMutableArray *unsortedSections = [NSMutableArray arrayWithCapacity:sectionCount];

	//create an array to hold the data for each section  
    for(int i = 0; i < sectionCount; i++)  
    {  
        [unsortedSections addObject:[NSMutableArray array]];  
    }

    //put each object into a section  
    for (id object in array)  
    {  
        NSInteger index = [collation sectionForObject:object collationStringSelector:selector];  
        [[unsortedSections objectAtIndex:index] addObject:object];  
    }
    NSMutableArray *sections = [NSMutableArray arrayWithCapacity:sectionCount];

    //sort each section  
    for (NSMutableArray *section in unsortedSections)  
    {  
        [sections addObject:[collation sortedArrayFromArray:section collationStringSelector:selector]];  
    }
    return sections;  
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView  
{  
    //we use sectionTitles and not sections  
    return [[[UILocalizedIndexedCollation currentCollation] sectionTitles] count];  
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section  
{  
    return [[self.tableData objectAtIndex:section] count];  
}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section  
{  
    BOOL showSection = [[self.tableData objectAtIndex:section] count] != 0;  

	//only show the section title if there are rows in the section  
    return (showSection) ? [[[UILocalizedIndexedCollation currentCollation] sectionTitles] objectAtIndex:section] : nil;  
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath  
{  

    id object = [[self.tableData objectAtIndex:indexPath.section] objectAtIndex:indexPath.row];  
    // configure the cell  
    //...
}  

3. Determine the correct section to go to for a given index

- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index  
{  

    //sectionForSectionIndexTitleAtIndex: is a bit buggy, but is still useable  

    return [[UILocalizedIndexedCollation currentCollation] sectionForSectionIndexTitleAtIndex:index];  
}

That’s the basics of UILocalizedIndexedCollation. I haven’t covered everything. The areas left to cover are work arounds for a few bugs with UILocalizedIndexedCollation, how to add a search section and integrating with Core Data, but I haven’t figured out an elegant, reusable solution yet. When I do I’ll write it up and post it here.