开发者

Sorting an NSArray like the MPMediaPickerController/iPod Library

I'm developing a custom UIViewController for iPhone that emulates a subset of the MPMediaPickerController for files in my application's local documents directory. In particular, I'm attempting to re-create the Songs tab. I've been successful in creating my n开发者_C百科ew controller, except I can't get the song titles to sort like they do in the iPod Library or the MPMediaPickerController. Here's an example of how the song names need to be sorted:

  1. Awesome Song Title
  2. Cool Song
  3. The Darkest Song Ever
  4. My Song Title
  5. A Really Cool Song
  6. Why Me?
  7. 4 Hours Wasted

As you can see, the sorting excludes the leading articles in the song titles and also places songs that begin with a numeric value at the end of the list. Can anyone suggest an efficient sorting function that takes these tules into account?


Since it looks like no one could offer up a solution, I thought I would post the solution I came up with. First, I created a model for my data:

@interface MyModel : NSObject
{
   NSString* _value;
   NSString* _sortableValue;
}

@property (nonatomic,copy) NSString* value;

- (NSString*)sortableValue;
- (NSString*)comparableString:(NSString*)str;

@end

They key to the model is the comparableString method, which gets used to create the sortableValue. Here's the implementation of the model:

@implementation MyModel

@synthesize value=_value;

-(void)dealloc
{
   [_value release];
   [_sortableValue release];

   [super dealloc];
}

- (void)setValue:(NSString*)value
{
   [_value release];
   _value = [value copy];
   [_sortableValue release];
   _sortableTValue = nil;
}

- (NSString*)sortableValue
{
   if (_sortableValue == nil)
      _sortableValue = [[self comparableString:_value] retain];

   return _sortableValue;
}

- (NSString*)comparableString:(NSString*)str
{
   if (str == nil)
      return nil;
   else if ([str length] == 0)
      return [NSString stringWithString:str];

   NSCharacterSet* numbersSet = [NSCharacterSet decimalDigitCharacterSet];
   if ([str rangeOfCharacterFromSet:numbersSet options:0 range:NSMakeRange(0, 1)].location != NSNotFound)
      return [NSString stringWithString:str];

   NSRange range = NSMakeRange(0, [str length]);

   if ([str compare:@"a " options:(NSAnchoredSearch|NSCaseInsensitiveSearch) range:NSMakeRange(0, 2)] == NSOrderedSame)
      range.location = 2;
   else if ([str compare:@"an " options:(NSAnchoredSearch|NSCaseInsensitiveSearch) range:NSMakeRange(0, 3)] == NSOrderedSame)
      range.location = 3;
   else if ([str compare:@"the " options:(NSAnchoredSearch|NSCaseInsensitiveSearch) range:NSMakeRange(0, 4)] == NSOrderedSame)
      range.location = 4;

   range.length -= range.location;

   NSCharacterSet* lettersSet = [NSCharacterSet letterCharacterSet];
   NSUInteger letterOffset = [str rangeOfCharacterFromSet:lettersSet options:0 range:range].location;
   if (letterOffset == NSNotFound)
      return [NSString stringWithString:str];

   letterOffset -= range.location;
   range.location += letterOffset;
   range.length -= letterOffset;

   return [str substringWithRange:range];
}

@end

In addition to the removing the leading articles from the string, it also removes any leading non-letter characters. I have a song in my iPod library entitled "$ell Your $oul", which ends up in the E section in the MPMediaPickerController. I'm not sure that's what I would have done had I crated the initial sorting algorithm, but I was going to consistency with the MPMediaPickerController, so there you go.

The final piece of the puzzle is the UILocalizedIndexedCollation class. This handy little helper class will help you sort your data to make supplying it to a UITableView via a UITableViewDataSource a piece of cake. Here's a snippet on how to use the UILocalizedIndexedCollation class in conjunction with the model:

   // tableData will contain an NSArray for each populated section in the table view
   NSMutableDictionary* tableData = [NSMutableDictionary dictionary];

   NSMutableArray* myArray = [NSMutableArray array];
   // Populate myArray with instances of MyModel

   UILocalizedIndexedCollation* indexer = [UILocalizedIndexedCollation currentCollation];
   for (MyModel* data in myArray)
   {
      NSInteger index = [indexer sectionForObject:data collationStringSelector:@selector(sortableValue)];
      NSNumber* key = [[NSNumber alloc] initWithInteger:index];
      NSMutableArray* array = [tableData objectForKey:key];
      if (array == nil)
      {
         array = [NSMutableArray new]; // Will be released after creating a sorted array in the following section
         [tableData setObject:array forKey:key];
      }

      [array addObject:data];
      [key release];
   }

   [tableData enumerateKeysAndObjectsUsingBlock:^(id key, id array, BOOL* stop)
   {
      NSMutableArray* sortedArray = [[indexer sortedArrayFromArray:array collationStringSelector:@selector(sortableValue)] mutableCopy];
      [tableData setObject:sortedArray forKey:key];
      [array release];
   }];

One quick note about UILocalizedIndexedCollation (from Apple's documentation):

If the application provides a Localizable.strings file for the current language preference, the indexed-collation object localizes each string returned by the method identified by selector.

So make sure you provide a Localizable.strings for each language you want to support, or your table view will only have sections A-Z and #.

It took me a while to work out all of the details on this, so I hope it becomes useful for other people. If you see any ways I can improve this, please let me know!


You may need to consider certain characters with accents as well such as è, é, ò, à, ù, ì.

So I slightly modified your code to incorporate this. Your code is a great contribution to all of us iphone developers

    - (NSString*)comparableString:(NSString*)str
{
    if (str == nil)
        return nil;
    else if ([str length] == 0)
        return [NSString stringWithString:str];

    NSCharacterSet* numbersSet = [NSCharacterSet decimalDigitCharacterSet];
    if ([str rangeOfCharacterFromSet:numbersSet options:0 range:NSMakeRange(0, 1)].location != NSNotFound)
        return [NSString stringWithString:str];

    NSRange range = NSMakeRange(0, [str length]);

    if ([str compare:@"a " options:(NSAnchoredSearch|NSCaseInsensitiveSearch) range:NSMakeRange(0, 2)] == NSOrderedSame)
        range.location = 2;
    else if ([str compare:@"an " options:(NSAnchoredSearch|NSCaseInsensitiveSearch) range:NSMakeRange(0, 3)] == NSOrderedSame)
        range.location = 3;
    else if ([str compare:@"the " options:(NSAnchoredSearch|NSCaseInsensitiveSearch) range:NSMakeRange(0, 4)] == NSOrderedSame)
        range.location = 4;

    range.length -= range.location;

    NSCharacterSet* lettersSet = [NSCharacterSet letterCharacterSet];
    NSUInteger letterOffset = [str rangeOfCharacterFromSet:lettersSet options:0 range:range].location;
    if (letterOffset == NSNotFound)
        return [NSString stringWithString:str];

    letterOffset -= range.location;
    range.location += letterOffset;
    range.length -= letterOffset;



//my modification starts here.........

    NSString * finalString = [str substringWithRange:range];
    NSString * firstCharString = [finalString substringToIndex:1];

    NSData * encodedData = [firstCharString dataUsingEncoding:NSASCIIStringEncoding allowLossyConversion:YES];
    NSString * encodedString = [[NSString alloc] initWithBytes:[encodedData bytes] length:[encodedData length] encoding:NSASCIIStringEncoding];
    if ([encodedString isEqualToString:@"?"]) {
        return finalString;
    }

    NSString * finalProcessedString = [finalString stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:encodedString];
    [encodedString release];
    return finalProcessedString;
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜