Welcome to another Objective-C Tuesdays. Last week, we looked at
sorting C arrays and NSArray
s. Today, we will continue looking at sorting
NSArray
s using
NSSortDescriptor
s.
As we saw last week, the sorting methods of
NSArray
require you to specify a comparator in one form or another. When sorting an
NSArray
of simple objects like
NSString
s or
NSDate
s, the comparators are usually pretty simple to write and common objects often have useful comparator methods like
-caseInsensitiveCompare:
and
-localizedCompare:
.
When sorting
NSArray
s of more complex objects, writing comparators is often more tedious and error-prone. Here's the interface for simple
Person
class:
// Person.h
@interface Person : NSObject
@property (strong) Address *address;
@property (strong) NSDate *birthdate;
@property (copy) NSString *firstName;
@property (copy) NSString *lastName;
@end
And here's the
Address
class used by
Person
:
// Address.h
@interface Address : NSObject
@property (copy, nonatomic) NSString *street;
@property (copy, nonatomic) NSString *city;
@property (copy, nonatomic) NSString *state;
@property (copy, nonatomic) NSString *country;
@property (copy, nonatomic) NSString *postalCode;
@end
If we have an
NSArray
of
Person
objects, we may want to sort them in
country
,
lastName
,
firstName
order. Here's one way to do that, using a comparator block:
// sort Person objects by lastName, firstName
Person *frodo = [Person new];
[frodo setFirstName:@"Frodo"];
[frodo setLastName:@"Baggins"];
// ...
[[frodo address] setCountry:@"Shire"];
Person *bilbo = [Person new];
[bilbo setFirstName:@"Bilbo"];
[bilbo setLastName:@"Baggins"];
// ...
[[bilbo address] setCountry:@"Shire"];
Person *legolas = [Person new];
[legolas setFirstName:@"Legolas"];
[legolas setLastName:@"Greenleaf"];
// ...
[[legolas address] setCountry:@"Mirkwood"];
NSArray *people = [NSArray arrayWithObjects:frodo, bilbo, legolas, nil];
NSArray *sortedPeople = [people sortedArrayUsingComparator:^(id item1, id item2) {
Person *person1 = item1;
Person *person2 = item2;
// NSComparisonResult is a typedef for int
NSComparisonResult result = [[[person1 address] country] compare:[[person2 address] lastName]];
if (result) {
return result;
}
result = [[person1 lastName] compare:[person2 lastName]];
if (result) {
return result;
}
result = [[person1 firstName] compare:[person2 firstName]];
if (result) {
return result;
}
return NSOrderedSame; // NSOrderedSame == 0
}];
// sortedPeople contains:
// Legolas Greenleaf (Mirkwood)
// Bilbo Baggins (Shire)
// Frodo Baggins (Shire)
The general pattern of a multi-field comparator is simple: check each field in turn, stop and return the comparison result if non-zero; if all fields are equal, return zero (or
NSOrderedSame
to be more descriptive). This quickly becomes tedious when you have many fields to sort by or you need to dig down into child or grandchild objects for fields.
Fortunately, there's an easier way to do this.
NSArray
has a method called
-sortedArrayUsingDescriptors:
that takes an array of
NSSortDescriptor
objects. Each
NSSortDescriptor
specifies a
key path and sort direction (ascending or descending). The order of
NSSortDescriptor
s in the array determines the precedence of each field. If you're not familiar with
Key Value Coding (KVC), you may not have encountered key paths before. KVC is similar reflection in Java and other dynamic languages. KVC allows you to get and set fields on an object using the field names as strings, called
keys. To access fields on child objects, you use keys separated by dots to form a key path; KVC knows how to drill down your object graph and access fields on child objects. There are a lot of interesting things you can do with KVC, but today we will stick to building an array of
NSSortDescriptor
s:
NSSortDescriptor *byCountry = [NSSortDescriptor sortDescriptorWithKey:@"address.country"
ascending:YES];
NSSortDescriptor *byLastName = [NSSortDescriptor sortDescriptorWithKey:@"lastName"
ascending:YES];
NSSortDescriptor *byFirstName = [NSSortDescriptor sortDescriptorWithKey:@"firstName"
ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObjects:byCountry, byLastName, byFirstName, nil];
Notice that the
byCountry
sort descriptor uses the key path
@"address.country"
: it will first get the value of the
address
property of the
Person
object, then get the
country
property of the
address
. Key paths can be as deep as your object graph.
Using the array of sort descriptors is easy:
NSArray *sortedPeople = [people sortedArrayUsingDescriptors:sortDescriptors];
// sortedPeople contains:
// Legolas Greenleaf (Mirkwood)
// Bilbo Baggins (Shire)
// Frodo Baggins (Shire)
This certainly makes creating complex sort criteria much easier, and you're not limited to the default comparator for a field. You can specify a selector for a comparator method on the field this way:
// specify a method to call on the lastName object
NSSortDescriptor *byLastName = [NSSortDescriptor sortDescriptorWithKey:@"lastName"
ascending:YES
selector:@selector(caseInsensitiveCompare:)];
Or for more specialized comparisons, you can pass in a
NSComparator
block this way:
// sort descriptor using length of last name
NSSortDescriptor *byLastNameLength = [NSSortDescriptor sortDescriptorWithKey:@"lastName"
ascending:YES
comparator:^(id item1, id item2) {
NSString *lastName1 = item1;
NSString *lastName2 = item2;
// cast result to NSComparisonResult so that the
// compiler infers the correct return type
return (NSComparisonResult) ([lastName1 length] - [lastName2 length]);
}];
Specifying complex sort orders with
NSSortDescriptor
s is the type of higher level, declarative code that is easy to write, easy to read and easy to maintain, and in most cases you should consider using
NSSortDescriptor
rather than writing your own comparator methods, functions or blocks.
Next time, we will look at sorting
NSMutableArray
s in place, rather than producing a sorted copy like the various
-sortedArray
methods.