Search code examples
iosobjective-csortingnsarray

Sort NSArray custom object on multiple properties


I have a custom object with multiple properties.

One property is a Type, for example an enum: A, B, C etc

These all have a Date too.

@property (assign) CustomType customType;
@property (nonatomic, strong) NSDate *startDate;
...

typedef NS_ENUM(NSInteger, CustomType) {
    A,
    B,
    C
};

I would like to put all objects of type 'C' first then order the rest by their Date DESC.

What is the best way to achieve this?

I've tried adding multiple sortDescriptors but I only want C objects at the top, then the rest ordered by date, this orders each grouping by date.

NSSortDescriptor *sortDescriptor1 =[[NSSortDescriptor alloc] initWithKey:@"customType" ascending:NO];
NSSortDescriptor *sortDescriptor2 =[[NSSortDescriptor alloc] initWithKey:@"startDate" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor1, sortDescriptor2, nil];
NSArray *sortedObjects = [_items sortedArrayUsingDescriptors:sortDescriptors];

Example

+-------------------------+
| C | 2017-08-16 12:48:53 |
| C | 2017-08-15 12:46:53 |
| B | 2017-08-16 12:48:53 |
| B | 2017-08-14 12:43:53 |
| A | 2017-08-15 12:46:53 |
| A | 2017-08-14 12:31:53 |
+-------------------------+

Would like

+-------------------------+
| C | 2017-08-16 12:48:53 |
| C | 2017-08-15 12:46:53 |
| A | 2017-08-16 12:48:53 |
| B | 2017-08-15 12:46:53 |
| A | 2017-08-14 12:43:53 |
| B | 2017-08-14 12:31:53 |
+-------------------------+

Also can I decide which customType I would like first, so I might want B to be at the top, then the rest just by date?


Would it be easier to split the array into two groups, of ALL C then the rest. Sort each sub group, then join them back together?

I can then choose which top group I want in the below predicate:

NSPredicate *pred1 = [NSPredicate predicateWithFormat:@"customType == %ld", C];
NSPredicate *pred2 = [NSPredicate predicateWithFormat:@"customType != %ld", C];
NSArray *top = [_items filteredArrayUsingPredicate:pred1];
NSArray *bottom = [_items filteredArrayUsingPredicate:pred2];

NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc] initWithKey:@"startDate" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor1, nil];
NSArray *sortedTop = [top sortedArrayUsingDescriptors:sortDescriptors];
NSArray *sortedBottom = [bottom sortedArrayUsingDescriptors:sortDescriptors];

//NSArray *items = [NSArray arrayWithObjects:sortedTop, sortedBottom, nil];

NSArray *newArray = @[];
newArray = [newArray arrayByAddingObjectsFromArray:sortedTop];
newArray = [newArray arrayByAddingObjectsFromArray:sortedBottom];

_items = [newArray mutableCopy];

Sort by Date https://stackoverflow.com/a/6084944/2895831

NSLog(@"nodeEventArray == %@", nodeEventArray);
NSSortDescriptor *dateDescriptor = [NSSortDescriptor sortDescriptorWithKey:@"startDate" ascending:NO];
NSArray *sortDescriptors = [NSArray arrayWithObject:dateDescriptor];
NSArray *sortedEventArray = [nodeEventArray sortedArrayUsingDescriptors:sortDescriptors];
NSLog(@"sortedEventArray == %@", sortedEventArray);

In the past I've grouped an array by a given property and used a custom sort on that single property.

How to use Custom ordering on NSArray

@property (nonatomic, strong) NSString *level;

NSArray *levels = [array valueForKeyPath:@"@distinctUnionOfObjects.level"];

NSArray *levelsSortOrder = @[@"Beginner", @"Basic", @"Intermediate", @"Expert", @"Advanced", @"Developer", @"Seminar"];

NSArray *sortedArray = [levels sortedArrayUsingComparator: ^(id obj1, id obj2){
    NSUInteger index1 = [levelsSortOrder indexOfObject: obj1];
    NSUInteger index2 = [levelsSortOrder indexOfObject: obj2];
    NSComparisonResult ret = NSOrderedSame;
    if (index1 < index2)
    {
        ret = NSOrderedAscending;
    }
    else if (index1 > index2)
    {
        ret = NSOrderedDescending;
    }
    return ret;
}];

Solution

  • I like sortedArrayUsingComparator:, and I don't think you can do what you want with the others methods.

    sortedArrayUsingComparator: gives really large actions on how sort. The logic behind it is: Compare two objects between them (obj1 and obj2). You decide the sort on theses two. It will iterate through the whole array, and by comparing each objects two by two it will have the sorted result. So in the block, you have to explicit exactly what define why is an object "prioritized" against the other one. Here CustomType is one of them, and is prioritized before startDate. So let's do it in this order.

    The implementation then:

    NSArray *sortedArray = [unsortedArray sortedArrayUsingComparator: ^(CustomClass *obj1, CustomClass2 *obj2){ 
        CustomType type1 = [obj1 customType]; 
        CustomType type2 = [obj2 customType]; 
        if (type1 == C && type2 == C)
        {
            return [[obj1 startDate] compare:[obj2 startDate]];
        }
        else if (type1 == C && type2 != C) 
        {
            return NSOrderedAscending;
        }
        else if (type1 != C && type2 == C)
        {
            return NSOrderedDescending;
        }
        else //if (type1 != C && type2 != C)
        {
            return [[obj1 startDate] compare:[obj2 startDate]];
        }
    }];
    

    With a little of refactorization:

    NSArray *sortedArray = [unsortedArray sortedArrayUsingComparator: ^(CustomClass *obj1, CustomClass2 *obj2){ 
        CustomType type1 = [obj1 customType]; 
        CustomType type2 = [obj2 customType]; 
        if (type1 == C && type2 != C) 
        {
            return NSOrderedAscending;
        }
        else if (type1 != C && type2 == C)
        {
            return NSOrderedDescending;
        }
        else //Either (type1 == C && type2 == C) OR (type1 != C && type2 != C)
        {
            //return [[obj1 startDate] compare:[obj2 startDate]];
            return [[obj2 startDate] compare:[obj1 startDate]];
        }
    }];
    

    Note that NSOrderedAscending and NSOrderedDescending may have to be reversed, I never know which one to use, I always test.

    I replace id in both blocks with your CustomClass because you already know the class of objects you have. This avoids a line of cast: CustomType type1 = [(CustomClass *)obj1 customType];