Sorting Arrays – Objective C — August 24, 2015

Sorting Arrays – Objective C

Using arrays is a staple of iOS development and there many tools at our disposal to do all manor of things with them. Below are some that I use often and find quite useful.

Sorting simple arrays of strings and numbers

NSArray *names = @[@"Harry", @"George", @"Xavier", @"Adam", @"Michael", @"Tim"];
NSArray *sortedNameArray = [names sortedArrayUsingSelector:@selector(compare:)];

for (NSString *name in sortedNameArray)
{
    NSLog(@"%@", name);
}

Which will print out:
2015-08-16 17:39:08.137 ArrayTEST[24935:755443] Adam
2015-08-16 17:39:08.138 ArrayTEST[24935:755443] George
2015-08-16 17:39:08.138 ArrayTEST[24935:755443] Harry
2015-08-16 17:39:08.138 ArrayTEST[24935:755443] Michael
2015-08-16 17:39:08.138 ArrayTEST[24935:755443] Tim
2015-08-16 17:39:08.139 ArrayTEST[24935:755443] Xavier

Luckily this will also work with NSNumbers. Also notice that in this example I have reversed the sort order by adding reverseObjectEnumerator.

NSArray *ages = @[@34.3, @45, @67.2, @12, @23, @74];
		NSArray *sortedAgesArray = [[[ages sortedArrayUsingSelector:@selector(compare:)] reverseObjectEnumerator] allObjects];
		
for (NSNumber *age in sortedAgesArray)
{
    NSLog(@"%@", name);
}

2015-08-16 17:39:08.139 ArrayTEST[24935:755443] 74
2015-08-16 17:39:08.139 ArrayTEST[24935:755443] 67.2
2015-08-16 17:39:08.139 ArrayTEST[24935:755443] 45
2015-08-16 17:39:08.139 ArrayTEST[24935:755443] 34.3
2015-08-16 17:39:08.140 ArrayTEST[24935:755443] 23
2015-08-16 17:39:08.140 ArrayTEST[24935:755443] 12

What would happen though if our array of strings had a mix of cases?

NSArray *names = @[@"Harry", @"George", @"Xavier", @"adam", @"michael", @"tim"];
NSArray *sortedNameArray = [names sortedArrayUsingSelector:@selector(compare:)];
	
for (NSString *name in sortedNameArray)
{
    NSLog(@"%@", name);
}

Here is the output
2015-08-16 17:53:49.929 ArrayTEST[25007:767608] George
2015-08-16 17:53:49.931 ArrayTEST[25007:767608] Harry
2015-08-16 17:53:49.931 ArrayTEST[25007:767608] Xavier
2015-08-16 17:53:49.931 ArrayTEST[25007:767608] adam
2015-08-16 17:53:49.931 ArrayTEST[25007:767608] michael
2015-08-16 17:53:49.931 ArrayTEST[25007:767608] tim

Well they are ordered but not how I wanted. Notice the order of precedence here, uppercase letters are ordered then lowercase. If we wanted to order the array disregarding case we can use localizedCaseInsensitiveCompare.

NSArray *names = @[@"Harry", @"George", @"Xavier", @"adam", @"michael", @"tim"];
NSArray *sortedNameArray = [names sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
	
for (NSString *name in sortedNameArray)
{
    NSLog(@"%@", name);
}

Giving us the output:
2015-08-16 18:02:43.434 ArrayTEST[25022:774253] adam
2015-08-16 18:02:43.435 ArrayTEST[25022:774253] George
2015-08-16 18:02:43.435 ArrayTEST[25022:774253] Harry
2015-08-16 18:02:43.435 ArrayTEST[25022:774253] michael
2015-08-16 18:02:43.435 ArrayTEST[25022:774253] tim
2015-08-16 18:02:43.435 ArrayTEST[25022:774253] Xavier


Sorting Arrays by Object Properties

For an example I am making a class called Developer that will have three properties; name (string), age (NSNumber), platform (string). The code below will generate 6 developer objects, add them to a NSMutableArray named Developers and print them out in the original order of the array.

#import <Foundation/Foundation.h>

@interface Developer: NSObject

@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *lastName;
@property (nonatomic, strong) NSString *platform;
@property (nonatomic, strong) NSNumber *age;

@end

@implementation Developer
@end

int main(int argc, const char * argv[])
{
	@autoreleasepool
	{
		NSArray *names = @[@"Harry", @"Adam", @"Xavier", @"Steve", @"Adam", @"Michael", @"Tim"];
		NSArray *lastNames = @[@"Smith", @"Jackson", @"Cameron", @"Atlee", @"Bevin", @"Potter", @"Cortez"];
		NSArray *platforms = @[@"iOS", @"iOS", @"Android", @".Net", @"Android", @"Tizen", @"iOS"];
		NSArray *ages = @[@34.3, @45, @67.2, @12, @23, @74, @32];
		
		NSMutableArray *developers = [[NSMutableArray alloc] init];
		for (NSInteger i = 0; i < [names count]; i++)
		{
			Developer *developer = [[Developer alloc] init];
			developer.name = names[i];
			developer.lastName = lastNames[i];
			developer.age = ages[i];
			developer.platform = platforms[i];
			
			[developers addObject:developer];
		}
		
		for (Developer *developer in developers)
		{
			NSLog(@"%@ %@, %@, %@",developer.name, developer.lastName, developer.platform, developer.age);
		}
	}
	
    return 0;
}

Giving us
2015-08-16 19:23:46.898 ArrayTEST[25222:819170] Harry Smith, iOS, 34.3
2015-08-16 19:23:46.899 ArrayTEST[25222:819170] Adam Jackson, iOS, 45
2015-08-16 19:23:46.899 ArrayTEST[25222:819170] Xavier Cameron, Android, 67.2
2015-08-16 19:23:46.900 ArrayTEST[25222:819170] Steve Atlee, .Net, 12
2015-08-16 19:23:46.900 ArrayTEST[25222:819170] Adam Bevin, Android, 23
2015-08-16 19:23:46.900 ArrayTEST[25222:819170] Michael Potter, Tizen, 74
2015-08-16 19:23:46.900 ArrayTEST[25222:819170] Tim Cortez, iOS, 32

So if we wanted to sort our array by name how would we do it? How about NSSortDescriptor? To do this we simply get an object of NSSortDescriptor and tell it what property we are sorting by in string form and if we want our data sorted in ascending order or not. Simple

NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey: @"name" ascending: YES];
		NSArray *sortedDevelopers = [developers sortedArrayUsingDescriptors: [NSArray arrayWithObject:sortDescriptor]];
		
		for (Developer *developer in sortedDevelopers)
		{
			NSLog(@"%@ %@, %@, %@",developer.name, developer.lastName, developer.platform, developer.age);
		}

Which will give us the following
2015-08-16 19:24:23.626 ArrayTEST[25230:819624] Adam Jackson, iOS, 45
2015-08-16 19:24:23.627 ArrayTEST[25230:819624] Adam Bevin, Android, 23
2015-08-16 19:24:23.627 ArrayTEST[25230:819624] Harry Smith, iOS, 34.3
2015-08-16 19:24:23.627 ArrayTEST[25230:819624] Michael Potter, Tizen, 74
2015-08-16 19:24:23.628 ArrayTEST[25230:819624] Steve Atlee, .Net, 12
2015-08-16 19:24:23.628 ArrayTEST[25230:819624] Tim Cortez, iOS, 32
2015-08-16 19:24:23.628 ArrayTEST[25230:819624] Xavier Cameron, Android, 67.2

Well its a start but look at the two Adams, their last names are not in alphabetical order. That is because we are only sorting by name and not lastName. Luckily the NSArray method sortedArrayUsingDescriptors can take an array of NSSortDescriptor to sort our data. Lets try again.

NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey: @"name" ascending: YES];
		NSSortDescriptor *secondSortDescriptor = [[NSSortDescriptor alloc] initWithKey: @"lastName" ascending: YES];
		NSArray *sortedDevelopers = [developers sortedArrayUsingDescriptors: [NSArray arrayWithObjects:sortDescriptor, secondSortDescriptor, nil]];
		
		for (Developer *developer in sortedDevelopers)
		{
			NSLog(@"%@ %@, %@, %@",developer.name, developer.lastName, developer.platform, developer.age);
		}

2015-08-16 19:29:52.785 ArrayTEST[25243:822769] Adam Bevin, Android, 23
2015-08-16 19:29:52.786 ArrayTEST[25243:822769] Adam Jackson, iOS, 45
2015-08-16 19:29:52.786 ArrayTEST[25243:822769] Harry Smith, iOS, 34.3
2015-08-16 19:29:52.786 ArrayTEST[25243:822769] Michael Potter, Tizen, 74
2015-08-16 19:29:52.787 ArrayTEST[25243:822769] Steve Atlee, .Net, 12
2015-08-16 19:29:52.787 ArrayTEST[25243:822769] Tim Cortez, iOS, 32
2015-08-16 19:29:52.787 ArrayTEST[25243:822769] Xavier Cameron, Android, 67.2

Lets try one more. Why not order our developers by what platform they work on and then by age?

NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey: @"platform" ascending: YES];
		NSSortDescriptor *secondSortDescriptor = [[NSSortDescriptor alloc] initWithKey: @"age" ascending: YES];
		NSArray *sortedDevelopers = [developers sortedArrayUsingDescriptors: [NSArray arrayWithObjects:sortDescriptor, secondSortDescriptor, nil]];
		
		for (Developer *developer in sortedDevelopers)
		{
			NSLog(@"%@ %@, %@, %@",developer.name, developer.lastName, developer.platform, developer.age);
		}

2015-08-16 19:35:25.610 ArrayTEST[25256:827191] Steve Atlee, .Net, 12
2015-08-16 19:35:25.611 ArrayTEST[25256:827191] Adam Bevin, Android, 23
2015-08-16 19:35:25.612 ArrayTEST[25256:827191] Xavier Cameron, Android, 67.2
2015-08-16 19:35:25.612 ArrayTEST[25256:827191] Michael Potter, Tizen, 74
2015-08-16 19:35:25.612 ArrayTEST[25256:827191] Tim Cortez, iOS, 32
2015-08-16 19:35:25.612 ArrayTEST[25256:827191] Harry Smith, iOS, 34.3
2015-08-16 19:35:25.612 ArrayTEST[25256:827191] Adam Jackson, iOS, 45

A third argument can be used when initiaising NSSortDescriptor, which allows a selector to be used to perform the comparison, for example caseInsensitiveCompare and localizedStandardCompare. We used the former earlier on to sort our names disregarding letter case, the latter should be used when dealing with strings that a user will see, as it takes into account localisation language features.

For example to add the caseInsensitiveCompare you would use:

NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES selector:@selector(caseInsensitiveCompare:)];

Pretty useful!