iosobjective-caddressbookui

How to prevent crash when selecting specific contact using AdressBookUI


I'm getting crash on this line.

    phoneNumber = CFBridgingRelease(ABMultiValueCopyValueAtIndex(numbers, index));

If the first phone number is selected I get index of 1 which is wrong. It should be 0 and therefore choses wrong number. If I select second number it gives index of -1 which crashes the app.

#pragma mark helper methods

- (void)didSelectPerson:(ABRecordRef)person identifier:(ABMultiValueIdentifier)identifier {
    NSString *phoneNumber = @"";
    ABMultiValueRef numbers = ABRecordCopyValue(person, kABPersonPhoneProperty);
    if (numbers) {
        if (ABMultiValueGetCount(numbers) > 0) {
            CFIndex index = 0;
            if (identifier != kABMultiValueInvalidIdentifier) {
                index = ABMultiValueGetIndexForIdentifier(numbers, identifier);
            }
            phoneNumber = CFBridgingRelease(ABMultiValueCopyValueAtIndex(numbers, index));
        }
        CFRelease(numbers);
    }
    self.numberTextField.text = [NSString stringWithFormat:@"%@", phoneNumber];
}

Solution

  • There is a bug in iOS 8.3 (and presumably previous version of iOS 8) when working on copies of contacts that have had phone numbers/emails removed. The documentation for ABPeoplePickerNavigationController states that:

    In iOS 8 and later bringing up a people-picker navigtion controller does not require the app to have access to a user’s contacts, and the user will not be prompted to grant access. If the app does not itself have access to the user’s contacts, a temporary copy of the contact selected by the user will be returned to the app.

    In my testing I had a contact which had three phone numbers (let's call them 111, 222 and 333). It appears that identifiers are fixed, stable zero-based values. Thus my three phone numbers were identifier 0 to 2. If a phone number is deleted the identifiers do not change. Zero-based indexes are used to access the current list of phone numbers (or emails etc.) and ABMultiValueGetIndexForIdentifier is used to convert an identifier into an index.

    In my test I deleted the first phone number, 111. This does not change the identifiers for the remaining phone numbers (222=1, 333=2).

    When I used ABPeoplePickerNavigationController and chose the first phone number (222) the delegate method peoplePickerNavigationController: didSelectPerson:property:identifier: correctly passed an identifier of 1. However, ABMultiValueGetIndexForIdentifier returned an index of 1, not 0 and my app then copied the phone number 333 as the one it thought the user had selected. If the user picked 333 then I was correctly passed an identifier of 2 but ABMultiValueGetIndexForIdentifier converted that to -1 and then an unprotected call to ABMultiValueCopyValueAtIndex crashed.

    So, when working on a copy of the contact (which is what happens in iOS 8 when the app has not been authorised to access the address book), iOS seems to be using identifiers based on the real contact, but indexes are based on the copy. The copy seems to have forgotten the previously-deleted phone number and the identifier-to-index mapping goes wrong if the user picks a phone number that was created after a previously-deleted phone number. It works if the user hasn't deleted phone numbers, or if they have deleted phone numbers after the one they pick.

    The workaround is to complicate the app by making it ask the user for permission to access the Address Book using ABAddressBookRequestAccessWithCompletion. Once granted, the app will not be given a copy of the selected contact and the identifier-to-index mapping works correctly.