More NSArray Sorting
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.