Question

In OS X (Mac 10.8.2) I have added a category to ABPerson to return phone numbers as an array of Phone objects. The Phone object has two properties, label and value. I want to get all phone numbers and filter them to include only those whose label matches an input.

If I use nested valueForKey: calls on either the entire array of ABPersons or on an individual person with the keys @"phones" and @"label", then I get the desired result. If, however, I concatenate the valueForKey:s to a single valueForKeyPath: then I still get the proper result when searching the entire array, but not when evaluating a single person object:

    #import <Cocoa/Cocoa.h>
    #import <AddressBook/AddressBook.h>

    @interface Phone : NSObject
    @property (retain) NSString *label;
    @property (retain) NSString *value;
    @end

    @implementation Phone
    - (void)dealloc {
        self.label = nil;
        self.value = nil;
        [super dealloc];
    }
    @end

    @interface ABPerson (phones)
    - (NSArray *)phones;
    @end

    @implementation ABPerson (phones)
    - (NSArray *)phones {
        NSMutableArray *phones = [NSMutableArray array];
        ABMultiValue *values = [self valueForProperty:kABPhoneProperty];
        for (NSInteger i = 0; i < [values count]; i++) {
            Phone *phone = [[Phone alloc] init];
            phone.label = ABLocalizedPropertyOrLabel([values labelAtIndex:i]);
            phone.value = [values valueAtIndex:i];
            [phones addObject:phone];
            [phone release];
        }
        return [NSArray arrayWithArray:phones];
    }
    @end

    int main(int argc, char *argv[]) {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

        NSArray *people = [[ABAddressBook sharedAddressBook] people];
        ABPerson *person = [people objectAtIndex:0];

        NSArray *a0 = [[people valueForKey:@"phones"] valueForKey:@"label"];
        NSArray *a1 = [people valueForKeyPath:@"phones.label"];
        NSLog(@"a0 == a1: %d", [a0 isEqualTo:a1]);

        NSArray *a2 = [[person valueForKey:@"phones"] valueForKey:@"label"];
        NSArray *a3 = [person valueForKeyPath:@"phones.label"];
        NSLog(@"a2 == a3: %d", [a2 isEqualTo:a3]);

        NSLog(@"people count                                         : %3ld", [people count]);
        NSLog(@"people phones label count   (valueForKey.valueForKey): %3ld", [a0 count]);
        NSLog(@"people phones label count           (valueForKeyPath): %3ld", [a1 count]);
        NSLog(@"person phones label count   (valueForKey.valueForKey): %3ld", [a2 count]);
        NSLog(@"person phones label count           (valueForKeyPath): %3ld", [a3 count]);
        NSLog(@"----------------------------------------------------------");
        NSLog(@"people phones label objectAtIndex:0 (valueForKeyPath): %@", [a1 objectAtIndex:0]);
        NSLog(@"person phones label         (valueForKey.valueForKey): %@", a2);
        NSLog(@"person phones label                 (valueForKeyPath): %@", a3);

        [pool release];
        return 0;
    }

This outputs:

2013-01-07 15:03:42.537 test[51919:303] a0 == a1: 1
2013-01-07 15:03:42.540 test[51919:303] a2 == a3: 0
2013-01-07 15:03:42.541 test[51919:303] people count                                         : 200
2013-01-07 15:03:42.543 test[51919:303] people phones label count   (valueForKey.valueForKey): 200
2013-01-07 15:03:42.544 test[51919:303] people phones label count           (valueForKeyPath): 200
2013-01-07 15:03:42.545 test[51919:303] person phones label count   (valueForKey.valueForKey):   2
2013-01-07 15:03:42.546 test[51919:303] person phones label count           (valueForKeyPath):   0
2013-01-07 15:03:42.547 test[51919:303] ----------------------------------------------------------
2013-01-07 15:03:42.548 test[51919:303] people phones label objectAtIndex:0 (valueForKeyPath): (
    mobile,
    home
)
2013-01-07 15:03:42.549 test[51919:303] person phones label         (valueForKey.valueForKey): (
    mobile,
    home
)
2013-01-07 15:03:42.551 test[51919:303] person phones label                 (valueForKeyPath): (null)

Why does [[person valueForKey:@"phones"] valueForKey:@"label"] not equal [person valueForKeyPath:@"phones.label"]? This is important because I want to use a predicate filter on all contacts using the full @"phone.label" key path like so:

    NSString *value = @"home";
    NSPredicate *predicate = [NSPredicate predicateWithFormat:@"ANY phones.label == %@", value];
    NSArray *a4 = [people filteredArrayUsingPredicate:predicate];

In this example, a4 is always empty (using the predicate without the "ANY" flag still results in an empty filtered array). I believe this is because filteredArrayUsingPredicate: is essentially evaluating each object (person) for valueForKeyPath: and, as noted above, this does not work on the individual person objects. Is this correct?

If someone can shed some light on why valueForKeyPath: on the ABPerson object is failing, or offer a predicate for filtering the people array that returns the desired results, I'd be very appreciative.

Was it helpful?

Solution

This is a fairly ancient problem in ABRecord. (See this thread on it from 2007; I believe probably goes back to the original code from 10.2.) They override valueForKeyPath: and it doesn't behave in the default way. The key has to listed in +properties. While you can add your own properties, that doesn't really help. It's going to return the result found in database. It's never going to call your category method. And adding your own properties modifies the AB database schema, so it's a really big hammer anyway. You definitely shouldn't go copying the phone numbers into a separate property.

My recommendation is to wrap ABPerson into another object and perform queries on that. This will give you much more control over the record.

BTW, to the obvious question "but why does it work when you call it on people?" That's because you're using another specially-written version of valueForKeyPath:, the one that NSArray overrides. It calls valueForKey: on each record, and ABPerson doesn't override valueForKey:, just valueForKeyPath:. Weird, huh?

If this causes you trouble, you should open a radar (bugreport.apple.com). That's the only way things like this get changed.

Here's another blog post addressing the same thing. He's subclassing ABPerson rather than wrapping it. Since ABPerson is toll-free bridged to ABPersonRef, and doesn't explicitly say that you can subclass it, I'd probably use a wrapper instead. But the blog post may give you some more insights into the issue, and subclassing may be fine. For some more on that, see this even more ancient thread on this problem.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top