14

My method of programmatically retrieving e-mail addresses from the Address Book no longer seems to work on iOS 6 devices. It worked in iOS 5 and oddly, still works in the iOS 6 Simulator. Is there a new way to programmatically retrieve contacts from a users' Address Book?

ABAddressBookRef addressBook = ABAddressBookCreate();
CFArrayRef allPeople = ABAddressBookCopyArrayOfAllPeople(addressBook);
CFIndex nPeople = ABAddressBookGetPersonCount(addressBook);

self.contacts = [[NSMutableArray alloc] init];

int contactIndex = 0;
for (int i = 0; i < nPeople; i++) {
    // Get the next address book record.
    ABRecordRef record = CFArrayGetValueAtIndex(allPeople, i);        

    // Get array of email addresses from address book record.
    ABMultiValueRef emailMultiValue = ABRecordCopyValue(record, kABPersonEmailProperty);
    NSArray *emailArray = (__bridge_transfer NSArray *)ABMultiValueCopyArrayOfAllValues(emailMultiValue);

    [self.contacts addObject:emailArray];
}

To clarify, the above does not crash, it simply returns no results. ABAddressBookCopyArrayOfAllPeople is empty. Thanks!

Keller
  • 17,051
  • 8
  • 55
  • 72

4 Answers4

52

I created a helper class, AddressBookHelper, to handle backward compatibility. Here are the guts:

-(BOOL)isABAddressBookCreateWithOptionsAvailable {
    return &ABAddressBookCreateWithOptions != NULL;
}

-(void)loadContacts {
    ABAddressBookRef addressBook;
    if ([self isABAddressBookCreateWithOptionsAvailable]) {
        CFErrorRef error = nil;
        addressBook = ABAddressBookCreateWithOptions(NULL,&error);
        ABAddressBookRequestAccessWithCompletion(addressBook, ^(bool granted, CFErrorRef error) {
            // callback can occur in background, address book must be accessed on thread it was created on
            dispatch_async(dispatch_get_main_queue(), ^{
                if (error) {
                    [self.delegate addressBookHelperError:self];
                } else if (!granted) {
                    [self.delegate addressBookHelperDeniedAcess:self];
                } else {
                    // access granted
                    AddressBookUpdated(addressBook, nil, self);
                    CFRelease(addressBook);
                }
            });
        });
    } else {
        // iOS 4/5
        addressBook = ABAddressBookCreate();
        AddressBookUpdated(addressBook, NULL, self);
        CFRelease(addressBook);
    }
}

void AddressBookUpdated(ABAddressBookRef addressBook, CFDictionaryRef info, void *context) {
    AddressBookHelper *helper = (AddressBookHelper *)context;
    ABAddressBookRevert(addressBook);
    CFArrayRef people = ABAddressBookCopyArrayOfAllPeople(addressBook);

    // process the contacts to return
    NSArray *contacts = ...    

    [[helper delegate] addressBookHelper:helper finishedLoading:contacts];
};
Christopher Pickslay
  • 17,523
  • 6
  • 79
  • 92
  • Updated: added some missing `CFRelease` calls to prevent leaking `AddressBook` in this code. – Christopher Pickslay Oct 09 '12 at 00:36
  • Hi, just wondering why the call to dispatch_async()? I've tried without it and it works perfectly, and also doesn't block my UI while the address book is reloading. – jklp Oct 20 '12 at 11:16
  • The [documentation](http://developer.apple.com/library/ios/Documentation/AddressBook/Reference/ABAddressBookRef_iPhoneOS/Reference/reference.html#//apple_ref/doc/uid/TP40007099-CH200-SW17) states that `The completion handler is called on an arbitrary queue. If your app uses an address book throughout the app, you are responsible for ensuring that all usage of that address book is dispatched to a single queue to ensure correct thread-safe operation.` In testing, I had some crashes (can't recall the details) that suggested thread safety issues, which led me to the docs. – Christopher Pickslay Oct 22 '12 at 19:04
  • I suppose it may be more correct to get the current queue using `dispatch_get_current_queue`, and reference that queue in the callback block. In my case, this code is always invoked on the main thread, so that wasn't necessary. I haven't noticed any UI blocking using this code in 2 apps--the UI updates immediately. But YMMV, depending what you're doing in your callback method. – Christopher Pickslay Oct 22 '12 at 19:07
  • Ah right yep that makes sense. Do you mind putting that in your answer? I'm not sure if many people will read through the comments to discover this :) – jklp Oct 24 '12 at 05:09
  • The [documentation for dispatch_get_current_queue](http://developer.apple.com/library/mac/#documentation/Darwin/Reference/ManPages/man3/dispatch_get_current_queue.3.html) says `The dispatch_get_current_queue() function is only recommended for debugging and logging purposes. Code must not make any assumptions about the queue returned, unless it is one of the global queues or a queue the code has itself created.`, so I'm inclined to leave it as is, assuming it's invoked on the main queue. I suppose the whole body of `loadContacts` could be dispatched to a locally created queue. – Christopher Pickslay Oct 31 '12 at 22:24
  • Why the call to ABAddressBookRevert? (BTW thanks much for this) – ettore Nov 19 '12 at 19:58
  • It probably isn't relevant for most use cases, but it's based on the [documentation for ABExternalChangeCallback](http://developer.apple.com/library/ios/documentation/AddressBook/Reference/ABAddressBookRef_iPhoneOS/Reference/reference.html#//apple_ref/doc/uid/TP40007099-CH992-SW1): "The addressBook object does not take any action to flush or synchronize cached state with the Address Book database. If you want to ensure that addressBook doesn’t contain stale values, use ABAddressBookRevert." – Christopher Pickslay Nov 21 '12 at 05:50
12

Probably related to the new privacy controls—as of iOS 6, on the device, an app can’t access the user’s contacts without their permission. From the documentation:

On iOS 6.0 and later, if the caller does not have access to the Address Book database:

• For apps linked against iOS 6.0 and later, this function returns NULL.

• For apps linked against previous version of iOS, this function returns an empty read-only database.

If you haven’t seen the permissions alert come up (“SomeApp would like access to your contacts”), it’s possible that the direct address-book APIs just assume that they don’t have access and silently fail; you might have to display something from the AddressBookUI framework to trigger it.

Noah Witherspoon
  • 57,021
  • 16
  • 130
  • 131
  • 2
    Yep, you're right. Need to use ABAddressBookRequestAccessWithCompletion(). How can I check if this method exists though (for < iOS 6)? I asked the followup question here: http://stackoverflow.com/questions/12517768/ios-obj-c-check-if-c-style-method-exists-of-type-mymethodname – Keller Sep 20 '12 at 17:32
12

Try with this: access to the address book must be granted before it can be access programmatically. Here is what I ended up doing.

  #import <AddressBookUI/AddressBookUI.h>

  // Request authorization to Address Book
  ABAddressBookRef addressBookRef = ABAddressBookCreateWithOptions(NULL, NULL);

  if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusNotDetermined) {
    ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef error) {
      // First time access has been granted, add the contact
      [self _addContactToAddressBook];
    });
  }
  else if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized) {
    // The user has previously given access, add the contact
    [self _addContactToAddressBook];
  }
  else {
    // The user has previously denied access
    // Send an alert telling user to change privacy setting in settings app
  }
Meet
  • 4,904
  • 3
  • 24
  • 39
0

Probably related to the new privacy controls, as of iOS 6, on the device, an app can’t access the user’s contacts without their permission.

Code:

-(void)addressBookValidation
{



NSUserDefaults *prefs=[NSUserDefaults standardUserDefaults];
ABAddressBookRef addressbook = ABAddressBookCreate();
__block BOOL accessGranted = NO;

if (ABAddressBookRequestAccessWithCompletion != NULL)
{
    if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusNotDetermined)
    {
        dispatch_semaphore_t sema = dispatch_semaphore_create(0);
        ABAddressBookRequestAccessWithCompletion(addressbook, ^(bool granted, CFErrorRef error)
                                                 {
                                                     accessGranted = granted;
                                                     dispatch_semaphore_signal(sema);
                                                 });
        dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
        dispatch_release(sema);
    }
    else if(ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized)
    {
        accessGranted = YES;
    }
    else if (ABAddressBookGetAuthorizationStatus()==kABAuthorizationStatusDenied)
    {
        accessGranted = NO;
    }
    else if (ABAddressBookGetAuthorizationStatus()==kABAuthorizationStatusRestricted){
        accessGranted = NO;
    }
    else
    {
        accessGranted = YES;
    }


}
else
{
    accessGranted = YES;
}
[prefs setBool:accessGranted forKey:@"addressBook"];

NSLog(@"[prefs boolForKey:@'addressBook']--->%d",[prefs boolForKey:@"addressBook"]);
[prefs synchronize];
CFRelease(addressbook);
}
Tiago Sippert
  • 1,324
  • 7
  • 24
  • 33
anu
  • 1