34

I am trying to understand the ABAdressBookCreateWithOptions and ABAddressBookRequestAccessWithCompletion methods in iOS 6.

The most information i have been able to find is the following, "To request access to contact data, call the ABAddressBookRequestAccessWithCompletion function after calling the ABAddressBookCreateWithOptions function."

I believe together these methods should alert the user to decide whether to allow the application access to contacts, however when I use them I am seeing no prompt.

Could someone provide some sample code of how these methods should be called together in a real world example? How do I create (CFDictionary) options? I have working code using the deprecated ABAddressBookCreate method, but need to update to iOS 6 to accommodate privacy concerns.

Thanks in advance to anyone who can shed some light here!

Andriy
  • 2,727
  • 2
  • 18
  • 29
codeqi
  • 753
  • 1
  • 6
  • 14

4 Answers4

83

Now that the NDA has been lifted, here is my solution for this for the where you need replace a method which returns an Array. (If you'd rather not block while the user is deciding and are ready to potentially rewrite some of your existing code, please look at David's solution below):

ABAddressBookRef addressBook = ABAddressBookCreate();

__block BOOL accessGranted = NO;

if (ABAddressBookRequestAccessWithCompletion != NULL) { // we're on iOS 6
    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 { // we're on iOS 5 or older
    accessGranted = YES;
}


if (accessGranted) {

    NSArray *thePeople = (__bridge_transfer NSArray*)ABAddressBookCopyArrayOfAllPeople(addressBook);
    // Do whatever you need with thePeople...

}

Hope this helps somebody...

FreeGor
  • 615
  • 12
  • 24
Engin Kurutepe
  • 6,604
  • 3
  • 32
  • 63
  • 4
    You shouldn't store the accessGranted result in the user defaults. The user can change the permission at any time by going into the Settings.app > Privacy > Contacts. The docs says "The user is only asked for permission the first time you request access. Later calls use the permission granted by the user." – Jean Regisser Sep 21 '12 at 08:09
  • 5
    +1 Since a lot of people will probably c&p (as I did), one more improvement should be replacing the now-deprecated ABAddressBookCreate with the now-available ABAddressBookCreateWithOptions – eladleb Sep 21 '12 at 13:30
  • 3
    Please note that ABAddressBookCreateWithOptions is available only in IOS 6+. – jlee Sep 24 '12 at 18:36
  • I like the idea of using the semaphore to force ABAddressBookRequestAccessWithCompletion to be modal, but on my device this code causes the app to hang, and only when the app exits does the ABAddressBookRequestAccessWithCompletion dialog box appear. Anyone have any thoughts on resolving that? – stdout Oct 07 '12 at 17:24
  • which device and iOS version are you on stdout? It didn't occur in our tests on iOS 5.0+ – Engin Kurutepe Oct 08 '12 at 14:43
  • 1
    The documentation on this is horrible. The ABAdressBook docs do not mention that you need to call this new API when you compile on ios6. – Tom Andersen Oct 25 '12 at 18:36
  • Agreed. The documentation is annoying. Thanks for the intel tho. This really helped. – The Lazy Coder Nov 01 '12 at 05:02
  • 3
    The last statement should be this: `NSArray *thePeople = (__bridge_transfer NSArray*)ABAddressBookCopyArrayOfAllPeople(addressBook);` – adbie Nov 13 '12 at 13:11
  • @EnginKurutepe, please tell me what will be the alternative solution for iOS 5??? i m working on lower version and this code badly throwing errors to me. :( – Tirth Feb 28 '13 at 19:27
  • @iHungry it shouldn't throw any errors if you're building against the latest SDK. Our app Moped is using these couple of lines almost exactly in production for iOS5 and iOS6. – Engin Kurutepe Mar 01 '13 at 11:41
  • 1
    Any specific reason why you are not using CFRelease(addressBook)? If I use this without releasing addressBook it leaks but if I add CFRelease(addressBook) it crashes. Anyone knows why that could happen? – Jernej Strasner Mar 19 '13 at 22:00
  • `dispatch_release` is deprecated as of `iOS6`. It isn't needed for iOS 6.0 and above – tipycalFlow Aug 01 '13 at 05:33
  • If the block you pass to ABAddressBookRequestAccessWithCompletion get put on the main thread's queue, then doesn't this code result in deadlock? The main thread is waiting (which is a blocking operation) for the semaphore to be signaled but it won't get signaled because the completion block that was place on the main queue can't run because the main thread is being blocked... – platypus Nov 01 '13 at 17:19
  • Thanks! I've used this code in this gist: https://gist.github.com/dirtyhenry/7547064 which also features an example of how to use the resulting `thePeople` array – Dirty Henry Nov 19 '13 at 15:38
23

Most answers I've seen to this question do crazy complicated things with GCD and end up blocking the main thread. It's not necessary!

Here's the solution I've been using (works on iOS 5 and iOS 6):

- (void)fetchContacts:(void (^)(NSArray *contacts))success failure:(void (^)(NSError *error))failure {
  if (ABAddressBookRequestAccessWithCompletion) {
    // on iOS 6

    CFErrorRef err;
    ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, &err);

    if (err) {
      // handle error
      CFRelease(err);
      return;
    }

    ABAddressBookRequestAccessWithCompletion(addressBook, ^(bool granted, CFErrorRef error) {
      // ABAddressBook doesn't gaurantee execution of this block on main thread, but we want our callbacks to be
      dispatch_async(dispatch_get_main_queue(), ^{
        if (!granted) {
          failure((__bridge NSError *)error);
        } else {
          readAddressBookContacts(addressBook, success);
        }
        CFRelease(addressBook);
      });
    });
  } else {
    // on iOS < 6

    ABAddressBookRef addressBook = ABAddressBookCreate();
    readAddressBookContacts(addressBook, success);
    CFRelease(addressBook);
  }
}

static void readAddressBookContacts(ABAddressBookRef addressBook, void (^completion)(NSArray *contacts)) {
  // do stuff with addressBook
  NSArray *contacts = @[];

  completion(contacts);
}
Nik
  • 5,591
  • 2
  • 29
  • 23
  • In all the examples no one ever seems to release the addressBook is it not necessary to call CFRelease(addressBook)? – todd Sep 15 '13 at 16:56
  • 1
    I think you need to release the CFError if it's non-NULL from ABAddressBookCreateWithOptions. Not sure about the ABAddressBookRequestAccessWithCompletion case. – dmaclach Feb 05 '14 at 00:23
  • Great code. Why is `readAddressBookContacts` a C-function, rather than a method like `- (void)fetchContacts:...`? – bcattle Sep 29 '14 at 22:11
  • How would I go about declaring this? When I use [self fetchContracts:nil failure:nil]; it will go through the whole code and then fail to go any further in the viewDidLoad, I even changed the completion(contacts); to completion: (contacts); which got it right to the end but it does not go any further in the viewdidload, Also for this particular code how do I go about setting some variables from a viewcontroller.h, it is only showing as undefined even if I use [Viewcontroller test], I heard using global variables works but I wanted to see if there was a better way – Ginzo Milani Dec 08 '14 at 10:00
22

The other high ranking answer has problems:

  • it unconditionally calls API that don't exist in iOS older than 6, so your program will crash on old devices.
  • it blocks the main thread, so your app is unresponsive, and not making progress, during the time the system alert s up.

Here's my MRC take on it:

        ABAddressBookRef ab = NULL;
        // ABAddressBookCreateWithOptions is iOS 6 and up.
        if (&ABAddressBookCreateWithOptions) {
          NSError *error = nil;
          ab = ABAddressBookCreateWithOptions(NULL, (CFErrorRef *)&error);
    #if DEBUG
          if (error) { NSLog(@"%@", error); }
    #endif
          if (error) { CFRelease((CFErrorRef *) error); error = nil; }
        }
        if (ab == NULL) {
          ab = ABAddressBookCreate();
        }
        if (ab) {
          // ABAddressBookRequestAccessWithCompletion is iOS 6 and up.
          if (&ABAddressBookRequestAccessWithCompletion) {
            ABAddressBookRequestAccessWithCompletion(ab,
               ^(bool granted, CFErrorRef error) {
                 if (granted) {
                   // constructInThread: will CFRelease ab.
                   [NSThread detachNewThreadSelector:@selector(constructInThread:)
                                            toTarget:self
                                          withObject:ab];
                 } else {
                   CFRelease(ab);
                   // Ignore the error
                 }
                 // CFErrorRef should be owned by caller, so don't Release it.
               });
          } else {
            // constructInThread: will CFRelease ab.
            [NSThread detachNewThreadSelector:@selector(constructInThread:)
                                     toTarget:self
                                   withObject:ab];
          }
        }
      }
DavidPhillipOster
  • 3,425
  • 1
  • 17
  • 17
  • @DavidePhillipOster, what will be the alternative solution for iOS 5.0? – Tirth Feb 28 '13 at 19:26
  • Surprised this was not upvoted at all! These are simple yet significant improvements in performance and compatibility +1 – ryan0 Mar 03 '13 at 01:23
  • FOR iOS5.0 or below, app should provide a custom mechanism to take user's consent on using AddressBook. App should ask from user about contact privacy at launch time. Behavior should be similar as iOS6.0 does, prompt should only be displayed once at the very first time app launches. After that app should use the persisted settings. You may store the settings in NSUserDefaults which is convenient. You should make a single utility method which checks for iOS version and decide automatically, if it is iOS6.0 or above use ABAddressBookGetAuthorizationStatus() or else use your custom settings. – Ansari Mar 13 '13 at 08:33
  • Hey David, I think you are potentially leaking a CFErrorRef or two in there. If ABAddressBookCreateWithOptions gives you back a non-nil error, I think you own it. I'm not actually sure in the callback case (I'm guessing not?) Documentation isn't clear on either of them, but the documentation in CFError.h definitely implies that you own it in the first case. – dmaclach Feb 05 '14 at 00:22
  • Thanks, dmaclach, you are quite right. Attempting to edit and fix. – DavidPhillipOster Feb 12 '14 at 23:04
2

This is peripherally related to the original question, but I have not seen it mentioned anywhere else, and it took me about two days to figure it out. If you register a callback for address book changes, it MUST be on the main thread.

For example, in this code, only sync_address_book_two() will ever be called:

ABAddressBookRequestAccessWithCompletion(_addressBook, ^(bool granted, CFErrorRef error) {
    if (granted) {
        ABAddressBookRegisterExternalChangeCallback (_addressBook, sync_address_book_one, NULL);
        dispatch_async(dispatch_get_main_queue(), ^{
            ABAddressBookRegisterExternalChangeCallback (_addressBook, sync_address_book_two, NULL);
        });
    }
});
Eli Burke
  • 2,519
  • 24
  • 23