ABRecordRef fields don't seem to be populated?
Asked Answered
E

1

6

[background: iOS 7, Xcode 5, both up to date as of Feb 2014. Test data is address book entries with multiple phone numbers and multiple addresses in addition to basic contact info, on an iPhone 5 (real device, not simulator)]

My goal is to use the AddressBookUI methods to allow a user to specify a contact, then use the various fields (addresses, phone numbers, etc) to populate a GUI in my code. The ABPeoplePickerNavigationController is the standard mechanism to allow the user to pick a contact by name. Doing so results in this delegate method being called:

- (BOOL)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker 
shouldContinueAfterSelectingPerson:(ABRecordRef)person

However if I examine the person record at this point, none of the multi-value fields have any data. So I extracted the RecordID, retrieved that, and the resulting ABRecordRef also has no multi-value fields filled in.

If I return YES from the delegate method, another UI is shown to the user with contact details displayed. Touching any field results in this delegate method call

- (BOOL)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker shouldContinueAfterSelectingPerson:(ABRecordRef)person 
property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifier

and that ABRecordRef has all the multi value fields filled in.

I can't find any information about the records being lazily loaded, or there being either a required delay, or permissions issue that would prevent the fields from being filled in. And the code I am using to examine the records is a method, so the exact same code which finds the values in the second instance fails to find it in the first.

Any suggestions about what may be going on, or how I can retrieve the full records without displaying the second UI to the user?

I am using Apple's QuickContacts sample code. Here are the additions and changes I have made.

+(NSMutableDictionary *)convertABRecordRef:(ABRecordRef)person
{
    // Initialize a mutable dictionary and give it initial values.
    NSMutableDictionary *contactInfoDict = [[NSMutableDictionary alloc] initWithCapacity:12];

    // Use a general Core Foundation object.
    CFTypeRef generalCFObject = ABRecordCopyValue(person, kABPersonFirstNameProperty);

    ABRecordID foundId = ABRecordGetRecordID(person);
    NSNumber *personIDNum = [NSNumber numberWithInteger:foundId];
    [contactInfoDict setObject:personIDNum forKey:@"recordID"];


    // Get the first name.
    if (generalCFObject)
    {
        [contactInfoDict setObject:(__bridge NSString *)generalCFObject forKey:@"firstName"];
        CFRelease(generalCFObject);
    }

    // Get the last name.
    generalCFObject = ABRecordCopyValue(person, kABPersonLastNameProperty);
    if (generalCFObject) {
        [contactInfoDict setObject:(__bridge NSString *)generalCFObject forKey:@"lastName"];
        CFRelease(generalCFObject);
    }

    generalCFObject = ABRecordCopyValue(person, kABPersonOrganizationProperty);
    if (generalCFObject) {
        [contactInfoDict setObject:(__bridge NSString *)generalCFObject forKey:@"companyName"];
        CFRelease(generalCFObject);
    }

    //ABMultiValueRef phones = ABRecordCopyValue(person, kABPersonPhoneProperty);
    //NSArray *numbers = (NSArray *)ABMultiValueCopyArrayOfAllValues(phones);


    // Get the phone numbers as a multi-value property.
    ABMultiValueRef phonesRef = ABRecordCopyValue(person, kABPersonPhoneProperty);
    CFIndex phoneCount = ABMultiValueGetCount(phonesRef);
    for (CFIndex i=0; i<ABMultiValueGetCount(phonesRef); i++) {

        CFStringRef currentPhoneLabel = ABMultiValueCopyLabelAtIndex(phonesRef, i);
        CFStringRef currentPhoneValue = ABMultiValueCopyValueAtIndex(phonesRef, i);

        if (CFStringCompare(currentPhoneLabel, kABPersonPhoneMobileLabel, 0) == kCFCompareEqualTo) {
            [contactInfoDict setObject:(__bridge NSString *)currentPhoneValue forKey:@"mobileNumber"];
        }

        if (CFStringCompare(currentPhoneLabel, kABHomeLabel, 0) == kCFCompareEqualTo) {
            [contactInfoDict setObject:(__bridge NSString *)currentPhoneValue forKey:@"homeNumber"];
        }

        if (CFStringCompare(currentPhoneLabel, kABWorkLabel, 0) == kCFCompareEqualTo) {
            [contactInfoDict setObject:(__bridge NSString *)currentPhoneValue forKey:@"workNumber"];
        }

        CFRelease(currentPhoneLabel);
        CFRelease(currentPhoneValue);
    }
    CFRelease(phonesRef);


    // Get the e-mail addresses as a multi-value property.
    ABMultiValueRef emailsRef = ABRecordCopyValue(person, kABPersonEmailProperty);
    for (int i=0; i<ABMultiValueGetCount(emailsRef); i++)
    {
        CFStringRef currentEmailLabel = ABMultiValueCopyLabelAtIndex(emailsRef, i);
        CFStringRef currentEmailValue = ABMultiValueCopyValueAtIndex(emailsRef, i);

        if (CFStringCompare(currentEmailLabel, kABHomeLabel, 0) == kCFCompareEqualTo) {
            [contactInfoDict setObject:(__bridge NSString *)currentEmailValue forKey:@"homeEmail"];
        }

        if (CFStringCompare(currentEmailLabel, kABWorkLabel, 0) == kCFCompareEqualTo) {
            [contactInfoDict setObject:(__bridge NSString *)currentEmailValue forKey:@"workEmail"];
        }

        CFRelease(currentEmailLabel);
        CFRelease(currentEmailValue);
    }
    CFRelease(emailsRef);


    // Get the first street address among all addresses of the selected contact.
    ABMultiValueRef addressRef =  ABRecordCopyValue(person, kABPersonAddressProperty);
    if (ABMultiValueGetCount(addressRef) > 0)
    {
        CFIndex numberOfAddresses = ABMultiValueGetCount(addressRef);
        for (CFIndex i=0; i<numberOfAddresses; i++)
        {
            CFStringRef label = ABMultiValueCopyLabelAtIndex(addressRef, i);
            if (label)
            {
                if (CFEqual(label, kABHomeLabel))
                {
                    NSDictionary *addressDict = (__bridge NSDictionary *)ABMultiValueCopyValueAtIndex(addressRef, 0);

                    [contactInfoDict setObject:[addressDict objectForKey:(NSString *)kABPersonAddressStreetKey] forKey:@"homeAddress"];
                    [contactInfoDict setObject:[addressDict objectForKey:(NSString *)kABPersonAddressZIPKey] forKey:@"homeZipCode"];
                    [contactInfoDict setObject:[addressDict objectForKey:(NSString *)kABPersonAddressCityKey] forKey:@"homeCity"];

                }
                else if (CFEqual(label, kABWorkLabel))
                {
                    NSDictionary *addressDict = (__bridge NSDictionary *)ABMultiValueCopyValueAtIndex(addressRef, 0);

                    [contactInfoDict setObject:[addressDict objectForKey:(NSString *)kABPersonAddressStreetKey] forKey:@"workAddress"];
                    [contactInfoDict setObject:[addressDict objectForKey:(NSString *)kABPersonAddressZIPKey] forKey:@"workZipCode"];
                    [contactInfoDict setObject:[addressDict objectForKey:(NSString *)kABPersonAddressCityKey] forKey:@"workCity"];
                }
                CFRelease(label);
            }
        }
    }
    CFRelease(addressRef);


    // If the contact has an image then get it too.
    if (ABPersonHasImageData(person)) {
        NSData *contactImageData = (__bridge NSData *)ABPersonCopyImageDataWithFormat(person, kABPersonImageFormatThumbnail);

        [contactInfoDict setObject:contactImageData forKey:@"image"];
    }


    return contactInfoDict;
}



// Displays the information of a selected person
- (BOOL)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker shouldContinueAfterSelectingPerson:(ABRecordRef)person
{
    // only returns a few fields, and none of the multi value ones  :-(
    NSMutableDictionary *results = [QuickContactsViewController convertABRecordRef:person];

    ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, NULL);
    ABRecordID foundId = ABRecordGetRecordID(person);
    ABRecordRef fullPerson = ABAddressBookGetPersonWithRecordID(addressBook, foundId);

    // also only returns a few fields!?
    NSMutableDictionary *selectedFromID = [QuickContactsViewController convertABRecordRef:fullPerson];


    return YES;
}

// Does not allow users to perform default actions such as dialing a phone number, when they select a person property.
- (BOOL)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker shouldContinueAfterSelectingPerson:(ABRecordRef)person 
                                property:(ABPropertyID)property identifier:(ABMultiValueIdentifier)identifier
{
    // returns all simple and multi-value fields!?   
    NSMutableDictionary *results = [QuickContactsViewController convertABRecordRef:person];

    return NO;
}

EDIT: Adding my solution (thanks Thorsten!).

- (BOOL)peoplePickerNavigationController:(ABPeoplePickerNavigationController *)peoplePicker shouldContinueAfterSelectingPerson:(ABRecordRef)person
{
    NSArray *allPersonRecords = (NSArray *)CFBridgingRelease(ABPersonCopyArrayOfAllLinkedPeople(person));
    NSLog(@"Count Linked People: %i", allPersonRecords.count);

    NSMutableDictionary *results = [[NSMutableDictionary alloc]initWithCapacity:12];
    for (int x=0; x<[allPersonRecords count]; x++)
    {
        ABRecordRef user = CFBridgingRetain([allPersonRecords objectAtIndex:x]);
        NSMutableDictionary *userFromArray = [QuickContactsViewController convertABRecordRef:user];
        [results addEntriesFromDictionary:userFromArray];  // mush them together
        CFRelease(user);
    }

    NSLog(@"finished! Total number of contact fields found:%d", [results count]);
    // do something with the data here...

    return NO;
}
Egor answered 18/2, 2014 at 5:54 Comment(5)
I tested you code with the simulator’s address book of Xcode 5.0.1, and it works fine: Even the 1st call to convertABRecordRef: returns all fields. So, could it be a multi-threading issue, e.g. that you open the addressbook not on the main thread and set this reference as the ABPeoplePickerNavigationController's addressbook property, which is then used on the main thread?Antagonistic
Reinhard, I know the code works in the simulator, which is why I mentioned in the first sentence that I am running on a real device. The exact same Apple sample code that works on the simulator fails on the device. I don't think it can be a multi-threading issue, since it isn't running the picker on a different thread.Egor
I just ran the code on a device that still had iOS 5 on it, using XCode 4 to build, linking against ios 6.1 with no change in observed behavior. Very frustratingEgor
Very interesting: I run your code under 7.0.4 on an real device. If I single step through convertABRecordRef:, everything works fine. If I stop at a breakpoint after returning from convertABRecordRef:, the result NSMutableArray is in both cases nil. So this seems to be an Apple problem, not yours!Antagonistic
Further explanation on the root cause of this: the test device has my facebook and email credentials, so the address book entries for most people ALSO have linked data coming from those sources. This happens behind-the-scenes in iOS 7 (and 6, and probably 5). So even though there is only one contact shown in the address book, the backing data is comprised of multiple person records, with the others drawn from the linked accounts. VERY confusing, but it explains why Thorsten's solution works -- by iterating over all the records, and combining them together, you get access to all the data.Egor
E
2

One person can consist of multiple people. Iterate through all people to get your data:

NSArray *people = (NSArray *)CFBridgingRelease(ABPersonCopyArrayOfAllLinkedPeople(person));
...
ABRecordRef user = CFBridgingRetain([people objectAtIndex:i]);
...
valuesRef = ABRecordCopyValue(user, kABPersonPhoneProperty);
valuesCount = 0;
if (valuesRef != nil) valuesCount = ABMultiValueGetCount(valuesRef);
...

HTH

Exteroceptor answered 25/2, 2014 at 4:10 Comment(5)
Thorsten, the sample data in this instance is a single person, not a person "consisting of multiple people" -- I don't even know what that is, unless you are referring to groups? Not part of this bug or codeEgor
Although you see only one contact in the contacts-app it might be that the data for that contact is stored on different "cards", e.g. name on the first "card", mobile-number and address on the second "card", etc.. These "cards" are "linked people" (although it is only one person). In your code you only check the first "card", so you get not all data which is available for that person. With "ABPersonCopyArrayOfAllLinkedPeople(person)" you get all "cards" of one person and therefore all data.Exteroceptor
easy test: put these two lines on top of your convertABRecordRef-method: NSArray *people = (NSArray *)CFBridgingRelease(ABPersonCopyArrayOfAllLinkedPeople(person)); NSLog(@"Count Linked People: %i", people.count); and run your code. If the people count is > 1 you have more than one "card" for that contact.Exteroceptor
Thorsten -- I apologize! I just had a chance to try your suggestion and you were correct. I am still flabbergasted why/how there can be multiple people for the record, if the picker only shows one entry (when using the search), but totally is the case. You get the bounty and my sincerest thanks.Egor
PS -- I just noticed your explanation of the "multiple people" situation. Thank you for that also!Egor

© 2022 - 2024 — McMap. All rights reserved.