Collection Operators can be used in a KVC key path to perform an operation on a “collection-type” property (i.e. NSArray
, NSSet
and similar). For example, a common operation to perform is to count the objects in a collection. To achieve this, you use the @count
collection operator:
self.array = @[@5, @4, @3, @2, @1];
NSNumber *count = [self.array valueForKeyPath:@"@count"];
NSNumber *countAlt = [self valueForKeyPath:@"array.@count"];
// count == countAlt == 5
While this is completely redundant here (we could have just accessed the count
property), it can be useful on occasion, though it is rarely necessary. There are, however, some collection operators that are much more useful, namely @max
, @min
, @sum
, @avg
and the @unionOf
family. It is important to note that these operators also require a separate key path following the operator to function correctly. Here's a list of them and the type of data they work with:
Operator | Data Type |
---|---|
@count | (none) |
@max | NSNumber , NSDate , int (and related), etc. |
@min | NSNumber , NSDate , int (and related), etc. |
@sum | NSNumber , int (and related), double (and related), etc. |
@avg | NSNumber , int (and related), double (and related), etc. |
@unionOfObjects | NSArray , NSSet , etc. |
@distinctUnionOfObjects | NSArray , NSSet , etc. |
@unionOfArrays | NSArray<NSArray*> |
@distinctUnionOfArrays | NSArray<NSArray*> |
@distinctUnionOfSets | NSSet<NSSet*> |
@max
and @min
will return the highest or lowest value, respectively, of a property of objects in the collection. For example, look at the following code:
// “Point” class used in our collection
@interface Point : NSObject
@property NSInteger x, y;
+ (instancetype)pointWithX:(NSInteger)x y:(NSInteger)y;
@end
...
self.points = @[[Point pointWithX:0 y:0],
[Point pointWithX:1 y:-1],
[Point pointWithX:5 y:-6],
[Point pointWithX:3 y:0],
[Point pointWithX:8 y:-4],
];
NSNumber *maxX = [self valueForKeyPath:@"[email protected]"];
NSNumber *minX = [self valueForKeyPath:@"[email protected]"];
NSNumber *maxY = [self valueForKeyPath:@"[email protected]"];
NSNumber *minY = [self valueForKeyPath:@"[email protected]"];
NSArray<NSNumber*> *boundsOfAllPoints = @[maxX, minX, maxY, minY];
...
In just a 4 lines of code and pure Foundation, with the power of Key-Value Coding collection operators we were able to extract a rectangle that encapsulates all of the points in our array.
It is important to note that these comparisons are made by invoking the compare:
method on the objects, so if you ever want to make your own class compatible with these operators, you must implement this method.
@sum
will, as you can probably guess, add up all the values of a property.
@interface Expense : NSObject
@property NSNumber *price;
+ (instancetype)expenseWithPrice:(NSNumber *)price;
@end
...
self.expenses = @[[Expense expenseWithPrice:@1.50],
[Expense expenseWithPrice:@9.99],
[Expense expenseWithPrice:@2.78],
[Expense expenseWithPrice:@9.99],
[Expense expenseWithPrice:@24.95]
];
NSNumber *totalExpenses = [self valueForKeyPath:@"[email protected]"];
Here, we used @sum
to find the total price of all the expenses in the array. If we instead wanted to find the average price we're paying for each expense, we can use @avg
:
NSNumber *averagePrice = [self valueForKeyPath:@"[email protected]"];
Finally, there's the @unionOf
family. There are five different operators in this family, but they all work mostly the same, with only small differences between each. First, there's @unionOfObjects
which will return an array of the properties of objects in an array:
// See "expenses" array above
NSArray<NSNumber*> *allPrices = [self valueForKeyPath:
@"[email protected]"];
// Equal to @[ @1.50, @9.99, @2.78, @9.99, @24.95 ]
@distinctUnionOfObjects
functions the same as @unionOfObjects
, but it removes duplicates:
NSArray<NSNumber*> *differentPrices = [self valueForKeyPath:
@"[email protected]"];
// Equal to @[ @1.50, @9.99, @2.78, @24.95 ]
And finally, the last 3 operators in the @unionOf
family will go one step deeper and return an array of values found for a property contained inside dually-nested arrays:
NSArray<NSArray<Expense*,Expense*>*> *arrayOfArrays =
@[
@[ [Expense expenseWithPrice:@19.99],
[Expense expenseWithPrice:@14.95],
[Expense expenseWithPrice:@4.50],
[Expense expenseWithPrice:@19.99]
],
@[ [Expense expenseWithPrice:@3.75],
[Expense expenseWithPrice:@14.95]
]
];
// @unionOfArrays
NSArray<NSNumber*> allPrices = [arrayOfArrays valueForKeyPath:
@"@unionOfArrays.price"];
// Equal to @[ @19.99, @14.95, @4.50, @19.99, @3.75, @14.95 ];
// @distinctUnionOfArrays
NSArray<NSNumber*> allPrices = [arrayOfArrays valueForKeyPath:
@"@distinctUnionOfArrays.price"];
// Equal to @[ @19.99, @14.95, @4.50, @3.75 ];
The one missing from this example is @distinctUnionOfSets
, however this functions exactly the same as @distinctUnionOfArrays
, but works with and returns NSSet
s instead (there is no non-distinct
version because in a set, every object must be distinct anyway).
And that's it! Collection operators can be really powerful if used correctly, and can help to avoid having to loop through stuff unnecessarily.
One last note: you can also use the standard collection operators on arrays of NSNumber
s (without additional property access). To do this, you access the self
pseudo-property that just returns the object:
NSArray<NSNumber*> *numbers = @[@0, @1, @5, @27, @1337, @2048];
NSNumber *largest = [numbers valueForKeyPath:@"@max.self"];
NSNumber *smallest = [numbers valueForKeyPath:@"@min.self"];
NSNumber *total = [numbers valueForKeyPath:@"@sum.self"];
NSNumber *average = [numbers valueForKeyPath:@"@avg.self"];