iPhone UITableView. How do turn on the single letter alphabetical list like the Music App?

In the iPhone music app, selecting Artist, Songs, or Albums presents a tableView with a verticl list of single letters at the righthand side of the UI that enables rapid scrolling. How do I enable this functionality in my app?

Cheers, Doug

61790 次浏览

Implement the delegate methods -sectionIndexTitlesForTableView: and -tableView:sectionForSectionIndexTitle:atIndex:

See the UITableViewDataSource documentation for more info.

Supply your own index characters:

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
return[NSArray arrayWithObjects:@"a", @"e", @"i", @"m", @"p", nil];
}

and then:

- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString
*)title atIndex:(NSInteger)index {
return <yourSectionIndexForTheSectionForSectionIndexTitle >;
}

You will need sections.

If you're using MonoTouch, override the SectionIndexTitles(UITableView) method in the UITableViewDataSource class. Just return an array of strings and the subclass takes care of the rest.

class TableViewDataSource : UITableViewDataSource
{
public override string[] SectionIndexTitles(UITableView tableView)
{
return new string[] { /*your string values */};
}
}

*just a hint for those of us using C# and Mono (.NET) to write iPhone apps. :)

A bunch of people asked if it was possible to do this without sections. I wanted the same thing and I found a solution which might be a little shady and doesn't return a value to sectionForSectionIndexTitle but if you are in a corner and don't want to have to make a section for every letter of the alphabet this is a sure fix. Sorry to any code Nazis in advance. :P

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
if (thisTableDataIsShowing)
{
NSMutableArray *charactersForSort = [[NSMutableArray alloc] init];
for (NSDictionary *item in d_itemsInTable)
{
if (![charactersForSort containsObject:[[item valueForKey:@"character_field_to_sort_by"] substringToIndex:1]])
{
[charactersForSort addObject:[[item valueForKey:@"character_field_to_sort_by"] substringToIndex:1]];
}
}
return charactersForSort;
}
return nil;
}


- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index {
BOOL found = NO;
NSInteger b = 0;
for (NSDictionary *item in d_itemsInTable)
{
if ([[[item valueForKey:@"character_field_to_sort_by"] substringToIndex:1] isEqualToString:title])
if (!found)
{
[d_yourTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:b inSection:0] atScrollPosition:UITableViewScrollPositionTop animated:NO];
found = YES;
}
b++;
}
}

It works great if you are getting a large amount of data and sectioning it would take a bunch of work. :) Tried to use generic variables so you knew what I was doing. d_itemsInTable is an NSArray of NSDictionaries that I'm listing out to the UITableView.

Something else you have to consider is localizing the sections for each language. After digging around a bit, I found UILocalizedIndexedCollation to be quite useful:

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return [[[UILocalizedIndexedCollation currentCollation] sectionTitles] objectAtIndex:section];
}


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


- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index {
return [[UILocalizedIndexedCollation currentCollation] sectionForSectionIndexTitleAtIndex:index];
}

https://developer.apple.com/documentation/uikit/uilocalizedindexedcollation

I came up with an alternative approach to handling a single letter alphabet list without using sections. It's similar to Zaph's answer but instead of getting any value from returning a new index (since we'll always have 1 section), we calculate the index for the location of the first item in the array that begins with a certain character, then scroll to it.

The downside is this requires searching the array every time (is this absolutely terrible?), however I didn't notice any lag or slow behavior in the iOS simulator or on my iPhone 4S.

- (NSArray *)sectionIndexTitlesForTableView:(UITableView *)tableView {
return[NSArray arrayWithObjects:@"A", @"B", @"C", @"D", @"E", @"F", @"G", @"H", @"I", @"J", @"K", @"L", @"M", @"N", @"O", @"P", @"Q", @"R", @"S", @"T", @"U", @"V", @"W", @"X", @"Y", @"Z", nil];
}


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


NSInteger newRow = [self indexForFirstChar:title inArray:self.yourStringArray];
NSIndexPath *newIndexPath = [NSIndexPath indexPathForRow:newRow inSection:0];
[tableView scrollToRowAtIndexPath:newIndexPath atScrollPosition:UITableViewScrollPositionTop animated:NO];


return index;
}


// Return the index for the location of the first item in an array that begins with a certain character
- (NSInteger)indexForFirstChar:(NSString *)character inArray:(NSArray *)array
{
NSUInteger count = 0;
for (NSString *str in array) {
if ([str hasPrefix:character]) {
return count;
}
count++;
}
return 0;
}

adding property to store last selected index like

 @property (assign, nonatomic) NSInteger previousSearchIndex;

and storing this property every time like:

- (NSInteger)indexForFirstChar:(NSString *)character inArray:(NSArray *)array
{
NSUInteger count = 0;
for (NSString *str in array) {
if ([str hasPrefix:character]) {
self.previousSearchIndex = count;
return count;
}
count++;
}
return self.previousSearchIndex;
}

and updating scrollToRow code like:

 [tableView scrollToRowAtIndexPath:newIndexPath atScrollPosition:UITableViewScrollPositionTop animated:YES];

Do this method even better and with nice animation.

Here is a modified version of Kyle's function that handles the case of clicking an index for which you do not have a string:

- (NSInteger)indexForFirstChar:(NSString *)character inArray:(NSArray *)array
{
char testChar = [character characterAtIndex:0];
__block int retIdx = 0;
__block int lastIdx = 0;


[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
char firstChar = [obj characterAtIndex:0];


if (testChar == firstChar) {
retIdx = idx;
*stop = YES;
}


//if we overshot the target, just use whatever previous one was
if (testChar < firstChar) {
retIdx = lastIdx;
*stop = YES;
}


lastIdx = idx;
}];
return retIdx;
}

If you're using a NSFetchedResultsController, you can just do:

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


- (NSInteger)tableView:(UITableView *)tableView sectionForSectionIndexTitle:(NSString *)title atIndex:(NSInteger)index {
return [frc sectionForSectionIndexTitle:title atIndex:index];
}

Here's a simple solution in Swift, assuming you have your title headers in an array. If the title couldn't be found, it will return the previous index in the array.

func sectionIndexTitlesForTableView(tableView: UITableView) -> [String]? {
return "ABCDEFGHIJKLMNOPQRSTUVWXYZ".characters.flatMap{String($0)}
}


func tableView(tableView: UITableView, sectionForSectionIndexTitle title: String, atIndex index: Int) -> Int {
return self.headerTitles.filter{$0 <= title}.count - 1
}