How do I correctly use ABAddressBookCreateWithOptions method in iOS 6?
Asked Answered
B

4

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!

Boresome answered 23/8, 2012 at 1:39 Comment(1)
no. its not. This question should be deleted anyhow. or at least amended to show the right answer.Seldan
F
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...

Fattish answered 20/9, 2012 at 10:22 Comment(14)
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."Psychotechnology
+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 ABAddressBookCreateWithOptionsIndividuation
Please note that ABAddressBookCreateWithOptions is available only in IOS 6+.Undry
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?Carnal
which device and iOS version are you on stdout? It didn't occur in our tests on iOS 5.0+Fattish
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.Kinnie
Agreed. The documentation is annoying. Thanks for the intel tho. This really helped.Seldan
The last statement should be this: NSArray *thePeople = (__bridge_transfer NSArray*)ABAddressBookCopyArrayOfAllPeople(addressBook);Carollcarolle
@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. :(Yorker
@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.Fattish
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?Electrocute
dispatch_release is deprecated as of iOS6. It isn't needed for iOS 6.0 and aboveVance
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...Vertu
Thanks! I've used this code in this gist: gist.github.com/dirtyhenry/7547064 which also features an example of how to use the resulting thePeople arrayRunthrough
F
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);
}
Flavourful answered 2/5, 2013 at 6:24 Comment(4)
In all the examples no one ever seems to release the addressBook is it not necessary to call CFRelease(addressBook)?Berton
I think you need to release the CFError if it's non-NULL from ABAddressBookCreateWithOptions. Not sure about the ABAddressBookRequestAccessWithCompletion case.Movable
Great code. Why is readAddressBookContacts a C-function, rather than a method like - (void)fetchContacts:...?Steeplejack
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 wayPowerboat
D
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];
          }
        }
      }
Decile answered 14/12, 2012 at 21:33 Comment(5)
@DavidePhillipOster, what will be the alternative solution for iOS 5.0?Yorker
Surprised this was not upvoted at all! These are simple yet significant improvements in performance and compatibility +1Oliana
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.Betray
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.Movable
Thanks, dmaclach, you are quite right. Attempting to edit and fix.Decile
L
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);
        });
    }
});
Linkboy answered 5/2, 2014 at 19:37 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.