Sunday, May 27, 2012

How to sort an NSMutableArray with custom objects in it?


What I want to do seems pretty simple, but I can't find any answers on the web. I have an NSMutableArray of objects, let's say they are 'Person' objects. I want to sort the NSMutable array by Person.birthDate which is an NSDate.



I think it has something to do with this method:




NSArray *sortedArray = [drinkDetails sortedArrayUsingSelector:@selector(???)];



In Java I would make my object implement Comparable, or use Collections.sort with an inline custom comparator...how on earth do you do this in Objective-C?


Source: Tips4all

13 comments:

  1. Compare method

    Either you implement a compare-method for your object:

    - (NSComparisonResult)compare:(Person *)otherObject {
    return [self.birthDate compare:otherObject.birthDate];
    }

    NSArray *sortedArray;
    sortedArray = [drinkDetails sortedArrayUsingSelector:@selector(compare:)];


    NSSortDescriptor (better)

    or usually even better: (The default sorting selector of NSSortDescriptor is compare:)

    NSSortDescriptor *sortDescriptor;
    sortDescriptor = [[[NSSortDescriptor alloc] initWithKey:@"birthDate"
    ascending:YES] autorelease];
    NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
    NSArray *sortedArray;
    sortedArray = [drinkDetails sortedArrayUsingDescriptors:sortDescriptors];


    Blocks (shiny!)

    There's also the possibility of sorting with a block since 10.6:

    NSArray *sortedArray;
    sortedArray = [drinkDetails sortedArrayUsingComparator:^(id a, id b) {
    NSDate *first = [(Person*)a birthDate];
    NSDate *second = [(Person*)b birthDate];
    return [first compare:second];
    }];

    ReplyDelete
  2. See the NSMutableArray method sortUsingFunction:context:

    You will need to set up a compare function which takes two objects (of type Person, since you are comparing two Person objects) and a context parameter.

    The two objects are just instances of Person. The third object is a string, e.g. @"birthDate".

    This function returns an NSComparisonResult: It returns NSOrderedAscending if PersonA.birthDate < PersonB.birthDate. It will return NSOrderedDescending if PersonA.birthDate > PersonB.birthDate. Finally, it will return NSOrderedSame if PersonA.birthDate == PersonB.birthDate.

    This is rough pseudocode; you will need to flesh out what it means for one date to be "less", "more" or "equal" to another date:

    NSComparisonResult compare(Person *firstPerson, Person *secondPerson, void *context) {
    if ([firstPerson birthDate] < [secondPerson birthDate])
    return NSOrderedAscending;
    else if ([firstPerson birthDate] > [secondPerson birthDate])
    return NSOrderedDescending;
    else
    return NSOrderedSame;
    }

    ReplyDelete
  3. I did this in iOS 4 using a block.
    Had to cast the elements of my array from id to my class type.
    In this case it was a class called Score with a property called points.

    Also you need to decide what to do if the elements of your array are not the right type, for this example I just returned NSOrderedSame, however in my code I though an exception.

    NSArray *sorted = [_scores sortedArrayUsingComparator:^(id obj1, id obj2){
    if ([obj1 isKindOfClass:[Score class]] && [obj2 isKindOfClass:[Score class]]) {
    Score *s1 = obj1;
    Score *s2 = obj2;

    if (s1.points > s2.points) {
    return (NSComparisonResult)NSOrderedAscending;
    } else if (s1.points < s2.points) {
    return (NSComparisonResult)NSOrderedDescending;
    }
    }

    // TODO: default is the same?
    return (NSComparisonResult)NSOrderedSame;
    }];

    return sorted;


    PS: This is sorting in descending order.

    ReplyDelete
  4. Your Person objects need to implement a method, say compare: which takes another Person object, and return NSComparisonResult according to the relationship between the 2 objects.

    Then you would call sortedArrayUsingSelector: with @selector(compare:) and it should be done.

    There are other ways, but as far as I know there is no Cocoa-equiv of the Comparable interface. Using sortedArrayUsingSelector: is probably the most painless way to do it.

    ReplyDelete
  5. Starting in iOS 4 you can also use blocks for sorting.

    For this particular example I'm assuming that the objects in your array have a 'position' method, which returns an NSInteger.

    NSArray *arrayToSort = where ever you get the array from... ;
    NSComparisonResult (^sortBlock)(id, id) = ^(id obj1, id obj2) {
    if ([obj1 position] > [obj2 position]) {
    return (NSComparisonResult)NSOrderedDescending;
    }
    if ([obj1 position] < [obj2 position]) {
    return (NSComparisonResult)NSOrderedAscending;
    }
    return (NSComparisonResult)NSOrderedSame;
    };
    NSArray *sorted = [arrayToSort sortedArrayUsingComparator:sortBlock];


    Note: the "sorted" array will be autoreleased.

    ReplyDelete
  6. I tried all, but this worked for me. In a class I have another class named "crimeScene", and want to sort by a property of "crimeScene".

    This works like a charm!

    NSSortDescriptor *sorter = [[NSSortDescriptor alloc] initWithKey:@"crimeScene.distance" ascending:YES];
    [self.arrAnnotations sortUsingDescriptors:[NSArray arrayWithObject:sorter]];

    ReplyDelete
  7. iOS 4 blocks will save you :)

    featuresArray = [[unsortedFeaturesArray sortedArrayUsingComparator: ^(id a, id b) {
    DMSeatFeature *first = ( DMSeatFeature* ) a;
    DMSeatFeature *second = ( DMSeatFeature* ) b;

    if ( first.quality == second.quality )
    return NSOrderedSame;
    else
    {
    if ( eSeatQualityGreen == m_seatQuality
    || eSeatQualityYellowGreen == m_seatQuality
    || eSeatQualityDefault == m_seatQuality )
    {
    if ( first.quality < second.quality )
    return NSOrderedAscending;
    else
    return NSOrderedDescending;
    }
    else // eSeatQualityRed || eSeatQualityYellow
    {
    if ( first.quality > second.quality )
    return NSOrderedAscending;
    else
    return NSOrderedDescending;
    }
    }
    }] retain];


    http://sokol8.blogspot.com/2011/04/sorting-nsarray-with-blocks.html a bit of description

    ReplyDelete
  8. there is a missing step in gs 2nd answer, works fine then.

    NSSortDescriptor *sortDescriptor;
    sortDescriptor = [[[NSSortDescriptor alloc] initWithKey:@"birthDate"
    ascending:YES] autorelease];
    NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
    NSArray *sortedArray;
    sortedArray = [drinkDetails sortedArrayUsingDescriptor:sortDescriptors];


    (I did not write in the comment because I just logged in and without reputation, I can't comment :-/)

    ReplyDelete
  9. For some reason the topmost answer didn't work for me. I was getting a warning on compile and it crashed at runtime. Here the code I used:

    NSSortDescriptor *dateSorter = [[NSSortDescriptor alloc] initWithKey:@"Date" ascending:YES];
    [filteredShowsList sortUsingDescriptors:[NSArray arrayWithObject:dateSorter]];


    Note that filteredShowsList has to be a NSMutableArray, not a NSArray!

    ReplyDelete
  10. NSSortDescriptor *sortDescriptor;
    sortDescriptor = [[[NSSortDescriptor alloc] initWithKey:@"birthDate" ascending:YES] autorelease];
    NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
    NSArray *sortedArray;
    sortedArray = [drinkDetails sortedArrayUsingDescriptors:sortDescriptors];


    thanks its working fine...

    ReplyDelete
  11. For NSMutableArray, use the sortUsingSelector method. It sorts it-place, without creating a new instance.

    ReplyDelete
  12. i've used sortUsingFunction:: in some of my projects

    int SortPlays(id a, id b, void* context)
    {
    Play* p1=a;
    Play* p2=b;
    if (p1.score<p2.score) return NSOrderedDescending;
    else if (p1.score>p2.score) return NSOrderedAscending;
    return NSOrderedSame;
    }

    ...
    [validPlays sortUsingFunction:SortPlays context:nil];

    ReplyDelete
  13. Sorting NSMutableArray is very simple..

    NSMutableArray *arrayToFilter = [[NSMutableArray arrayWithObjects:@"Photoshop", @"Flex", @"AIR",@"Flash", @"Acrobat", nil] autorelease];

    NSMutableArray *productsToRemove = [[NSMutableArray array] autorelease];
    for (NSString *products in arrayToFilter) {
    if (fliterText && [products rangeOfString:fliterText options:NSLiteralSearch|NSCaseInsensitiveSearch].length == 0)
    [productsToRemove addObject:products];
    }
    [arrayToFilter removeObjectsInArray:productsToRemove];

    ReplyDelete