Objective-C Language Key Value Coding / Key Value Observing Collection Operators


Example

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:

OperatorData Type
@count(none)
@maxNSNumber, NSDate, int (and related), etc.
@minNSNumber, NSDate, int (and related), etc.
@sumNSNumber, int (and related), double (and related), etc.
@avgNSNumber, int (and related), double (and related), etc.
@unionOfObjectsNSArray, NSSet, etc.
@distinctUnionOfObjectsNSArray, NSSet, etc.
@unionOfArraysNSArray<NSArray*>
@distinctUnionOfArraysNSArray<NSArray*>
@distinctUnionOfSetsNSSet<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:@"points.@max.x"];
NSNumber *minX = [self valueForKeyPath:@"points.@min.x"];
NSNumber *maxY = [self valueForKeyPath:@"points.@max.y"];
NSNumber *minY = [self valueForKeyPath:@"points.@min.y"];

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:@"expenses.@sum.price"];

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:@"expenses.@avg.price"];

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:
    @"expenses.@unionOfObjects.price"];

// 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:
    @"expenses.@distinctUnionOfObjects.price"];

// 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 NSSets 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 NSNumbers (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"];