Search code examples
iosobjective-cnspredicatedistinct-values

distinctUnionOfObjects not working


I have created simple XCTest to test distinctUnionOfObjects. All the test cases are passing except one which is isKindOfClass (Last XCTAssertTrue). Any idea why it's changing the class when you do distinctUnionOfObjects.

- (void)testUsersPredicate
{
         NSArray *usersBeforePredicate = [[self userData] users];
         XCTAssertEqual([usersBeforePredicate count] , 34u, @"We need 34");
         XCTAssertTrue([[usersBeforePredicate lastObject] isKindOfClass:[ICEUsersModelObject class]], @"Object is not ICEUsersModelObject class");

         NSString *distinctUsersKeyPath = [NSString stringWithFormat:@"@distinctUnionOfObjects.%@", @"userName"];
         NSArray* usersAfterPredicate = [usersBeforePredicate valueForKeyPath:distinctUsersKeyPath];

         XCTAssertEqual([usersAfterPredicate count] , 30u, @"We need 30");
         XCTAssertTrue([[usersAfterPredicate lastObject] isKindOfClass:[ICEUsersModelObject class]], @"Object is not ICEUsersModelObject class");
}

Solution

  • As the right key path on your distinctUnionOfObjects is userName, the -valueForKeyPath: call will return an NSArray of distinct userNames (not user objects).

    From Apple's KVC Programming Guide:

    The @distinctUnionOfObjects operator returns an array containing the distinct objects in the property specified by the key path to the right of the operator.

    Change the last test case to check for [NSString class] and it should pass.

    Alternatives

    Using equality:

    If the userNameproperty is supposed to serve as a unique identifier, you could enforce that by overriding -isEqual: and -hashon the user object to reflect this:

    - (BOOL)isEqual:(id)object {
        return ([object isKindOfClass:self.class] && [object.userName isEqual:self.userName]);
    }
    
    - (NSUInteger)hash {
        return self.userName.hash;
    }
    

    This can benefit your overall model design and opens up a lot of additional options, like this one that obtains a collection of distinct users reg. userName in one line - NSSet is very fast when used for this:

    NSArray *uniqueUsers = [[NSSet setWithArray:users] allObjects];
    
    • Note: I re-used the hashing function of NSString for the user hash, which has a subtle pitfall; -[NSString hash] only guarantees uniqueness for strings of up to 96 characters! This is not in the docs and took me almost a day to track down in production code. (see Apple's implementation of CFString.c - search for __CFStrHashCharacters)

    Using NSPredicate:

    Here's a, let's say, 'creative' solution that uses a predicate. However, some kind of iteration is needed, because the predicate condition would otherwise have to be a function of its own result:

    NSMutableArray *__uniqueUsers = [NSMutableArray array];
    [[users valueForKeyPath:@"@distinctUnionOfObjects.userName"] enumerateObjectsUsingBlock:^(id name, NSUInteger idx, BOOL *stop) {
        NSArray *uniqueUser = [users filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id user, NSDictionary *bindings) {
            return [user.userName isEqual:name];
        }]];
        if (uniqueUser.count > 0)
            [__uniqueUsers addObject:uniqueUser.lastObject];
    }];
    NSArray *uniqueUsers = [NSArray arrayWithArray:__uniqueUsers];
    

    It obtains a collection of unique userNames, iterates over it, selects exactly one user for each name and adds that to the output array.