Address book change callback registered with ABAddressBookRegisterExternalChangeCallback is never called (iOS 8) - ios

I've found plenty of examples around this but after reading the entire ABAddressBook documentation I'm still not able to figure out why, in my case, my change callback is not being called. I simply set up an address book and register a callback function for it.
I can access the address book just fine, but the callback function is never called no matter how much I change contacts in the Contacts app and then reopen my app. Is there any reason that the callback would never be called? I've already made sure I don't release the address book or unregister the callback.
The init code:
// Set up address book API.
CFErrorRef *error = NULL;
_addressBook = ABAddressBookCreateWithOptions(NULL, error);
if (error) {
NSLog(#"Could not initialize address book: %#", CFBridgingRelease(CFErrorCopyFailureReason(*error)));
} else {
ABAddressBookRegisterExternalChangeCallback(_addressBook, RogerAddressBookChangeCallback, (__bridge void *)self);
NSLog(#"Registered callback");
}
The callback function:
void RogerAddressBookChangeCallback(ABAddressBookRef addressBook, CFDictionaryRef info, void *context) {
NSLog(#"Address book change");
ABAddressBookRevert(addressBook);
RogerAddressBook *instance = (__bridge RogerAddressBook *)context;
[instance import];
}
I see the log output Registered callback but never Address book change.

Actually the code for ABAddressBook is written in C. So you may find difficulties in using the Original ABAddressBook Framework.
So I suggest for using an third party library (Which is just an makeover of C to Obj-C) to access contacts and the Contacts Change.
Here is the link for an popular library https://github.com/Alterplay/APAddressBook
Using the above framework you could easily observe the changes in Address book.
Observe address book external changes
// start observing
[addressBook startObserveChangesWithCallback:^
{
NSLog(#"Address book changed!");
}];
// stop observing
[addressBook stopObserveChanges];
This library also has lot of options like sorting, filtering etc.

Access to address book requires user authorization. If authorization status is kABAuthorizationStatusNotDetermined, your code fails silently, returning non-nil result and producing no error.
I have next code to create address book:
- (ABAddressBookRef)newAddressBookRef
{
ABAuthorizationStatus authorizationStatus = ABAddressBookGetAuthorizationStatus();
if (authorizationStatus == kABAuthorizationStatusAuthorized)
{
ABAddressBookRef addressBookRef = nil;
CFErrorRef error;
addressBookRef = ABAddressBookCreateWithOptions(NULL, &error);
return addressBookRef;
}
return nil;
}
And next code to explicitly request address book access (usually performed on application start).
typedef void(^AddressBookHelperAccessRequestCompletionHandler)(BOOL accessGiven);
- (void)requestAccessToAddressBook:(ABAddressBookRef)addressBookRef withCompletionHandler:(AddressBookHelperAccessRequestCompletionHandler)completionHandler
{
ABAuthorizationStatus authorizationStatus = ABAddressBookGetAuthorizationStatus();
switch (authorizationStatus)
{
case kABAuthorizationStatusNotDetermined:
{
// Request access permissions for even for NULL address book reference.
// When permissions have not been granted yet, all address book references will be equal to NULL
ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef error)
{
if (granted)
{
[self registerForAddressBookChanges];
}
if (completionHandler)
{
completionHandler(granted);
}
});
break;
}
case kABAuthorizationStatusDenied:
case kABAuthorizationStatusRestricted:
[self showNoContactsAccessAlert];
default:
{
if (completionHandler)
{
completionHandler(authorizationStatus == kABAuthorizationStatusAuthorized);
}
break;
}
}
}

Related

iOS accessing AddressBook Contacts via UnitTest; how to set permissions?

This question pertains to using the iPhone Simulator with Unit Tests. I have written a series of tests that test storing of our data, merged or not merged with data we can access from the user's contacts, depending on whether or not the contact exists. I would like a way to set the permissions such that I can test (A) when the user gives permission to access the contacts and (B) when the user denies access to the contacts. What I would like is a way, in Unit Tests only, to hard-code the permission value. I don't want to prompt for it, since that would block the unit test from running with the additional hardship that the permission remains set to that value forever.
So I am reaching out to the SO community to see who else might be testing their code's interaction with address book contacts by controlling the permissions in a Unit Test. Does anyone have a recipe which allows me test both sides of the user giving and denying access to the Address Book contacts?
I am using the XCTestCase parent class. I would be open to using something else if that helped solved this problem.
I have read through all the related SO questions and answers in this area. They are focused on helping people with writing the permission-asking routines and accessing the address book contacts in their application. I know how to do that part. I am specifically talking about how to fake the address book permissions in a unit test.
At first as I do this
1) in app ->
if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusDenied) ...
if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusRestricted) ...
2) for test this issues I use class of OCMock, for predefined values and methods
link here - http://ocmock.org
if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusDenied ||
ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusRestricted){
} else if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized){
} else{
ABAddressBookRequestAccessWithCompletion(ABAddressBookCreateWithOptions(NULL, nil), ^(bool granted, CFErrorRef error) {
if (!granted){
return;
}
});
}
i found the preceding code on a handy little website called:
www.raywenderlich.com
// Address Book Authorization grant
if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusDenied ||
ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusRestricted)
{
NSLog(#"Denied");
}
else if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized)
{
NSLog(#"Authorized");
}
else
{
ABAddressBookRequestAccessWithCompletion(ABAddressBookCreateWithOptions(NULL, nil), ^(bool granted, CFErrorRef error)
{
if (!granted) {
NSLog(#"Just denied");
return;
}
NSLog(#"Just authorized");
});
NSLog(#"Not determined");
}
For Unit testing
Nsstring *authorizationStatus;
if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusDenied ||
ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusRestricted){
//1
NSLog(#"Denied");
authorizationStatus = #"Denied";
} else if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized){
NSLog(#"Authorized");
authorizationStatus = #"Authorized";
} else{ //ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusNotDetermined
//3
NSLog(#"Not determined");
authorizationStatus = #"Not determined";
}
XCTAssertTrue(authorizationStatus, #"Authorized");

Store users contacts in NSMutable array after access is granted for address book

I'm using this in my viewWillAppear: to ask the user for permission/access(as apple requires) to their address book. When they have allowed access, they can proceed to an "invites" page, where they can browse through (their own)contacts that already have existing accounts on my app. Basically, I'm just going to take their contacts and put it into a UITableView. SO, I MUST GET ALL THEIR CONTACTS AND ADD IT TO AN array of type NSMutableArray. In the following code, where it says "//ADD ALL OF USERS CONTACTS TO ARRAY HERE" I need some code. What do I put there?
- (void)viewWillAppear:(BOOL)animated
{
// Request authorization to Address Book
ABAddressBookRef addressBookRef = ABAddressBookCreateWithOptions(NULL, NULL);
if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusNotDetermined) {
ABAddressBookRequestAccessWithCompletion(addressBookRef, ^(bool granted, CFErrorRef error) {
if (granted) {
// First time access has been granted, add user's contacts to array
// ADD ALL OF USERS CONTACTS TO ARRAY HERE
} else {
// User denied access
// Display an alert telling user that they must allow access in order to proceed to "invites" page
}
});
}
else if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized) {
// The user has previously given access, grab all contacts
// ADD ALL OF USERS CONTACTS TO ARRAY HERE
}
else {
// The user has previously denied access
// Display an alert telling user that they must allow access in order to proceed to "invites" page
}
}
You want ABAddressBookCopyArrayOfAllPeople().

Magical Record add object, different context error

I'm using Magical Record in my app, and want to add the functionality for a user to add a 'Note', which is a child of 'entry'.
I added this code:
[MagicalRecord saveWithBlock: ^(NSManagedObjectContext *localContext) {
Note *newNote = [Note MR_createInContext: localContext];
newNote.content = noteContent;
newNote.name = #"User Note";
[self.entry addNotesObject: newNote];
}
completion: ^(BOOL success, NSError *error) {
if (error != nil)
{
// show alert
}
else if (success)
{
[[self tableView] reloadData];
}
}];
The error I keep getting on the last line is "Illegal attempt to establish a relationship 'entry' between objects in different contexts"
I tried setting the context of both 'entry' and 'newNote' to 'localContext', but I still get the same error.
What am I missing?
self.entry was created in different context, so you can't access it from this one.
Instead of:
[self.entry addNotesObject: newNote];
you should first find self.entry object in localContext:
[[self.entry MR_inContext:localContext] addNotesObject: newNote];
You can find an explanation of using MagicalRecord in a concurrent environment at Performing Core Data operations on Threads. Though it's quite short, so in my opinion it's worthwhile to read Core Data Programming Guide even though you don't use CD directly.

Catch 22 according to the documentation for address book use with iOS 6

According to the documentation with iOS6 an address book should be created using ABAddressBookCreateWithOptions.
It also says if the caller does not have access to the db then this method will return null.
However access is requested by calling ABAddressBookRequestAccessWithCompletion, which takes an ABAddressBookRef as a parameter.
So according to the documentation you can't get an ABAddressBookRef in the first place if you don't have access, but to get access you have to have an ABAddressBookRef to pass as a parameter.
Eh. Catch 22? How do you create an ABAddressBookRef then?
Been googling for some example/tutorial code for this but haven't found any.
TIA
I use code like this:
if (ABAddressBookCreateWithOptions) {
_book = ABAddressBookCreateWithOptions(NULL, NULL);
if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusNotDetermined) {
ABAddressBookRequestAccessWithCompletion(_book, ^(bool granted, CFErrorRef error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (granted && !error) {
ABAddressBookRevert(_book);
}
});
});
}
} else {
_book = ABAddressBookCreate();
}
where _book is my ABAddressBookRef.
If my app has been denied access then _book will be NULL and you can't access the address book.
See my answer here:
https://stackoverflow.com/a/13671852/341994
ABAddressBookRequestAccessWithCompletion is useless and should be ignored.
If you need to know the authorization status, call ABAddressBookGetAuthorizationStatus.
If the authorization is undetermined (the user has never run this app), just attempt to access the database. This will cause the authorization alert to appear.
If the user has authorized or denied access, ABAddressBookRequestAccessWithCompletion does nothing of any value so there's no point calling it.

Can't Access Contacts Sources on Device in iOS 6

This code worked OK on iOS 5.1 and also does work in the iPhone simulator with iOS 6. It fails silently on my iPhone 4 running iOS 6. The end result is that I cannot add a person to the Contacts app. Neither of the following code snippets work (log follows each):
ABRecordRef defaultSource = ABAddressBookCopyDefaultSource(_addressBook);
NSLog(#"2 - defaultSource = %#", defaultSource);
AB: Could not compile statement for query (ABCCopyArrayOfAllInstancesOfClassInSourceMatchingProperties):
SELECT ROWID, Name, ExternalIdentifier, Type, ConstraintsPath, ExternalModificationTag, ExternalSyncTag, AccountID, Enabled, SyncData, MeIdentifier, Capabilities FROM ABStore WHERE Enabled = ?;
2012-09-24 11:00:36.731 QR vCard[193:907] 2 - defaultSource = (CPRecord: 0x1f59fd50 ABStore)
When I try to add a person to the Address Book I get this (seems to be because the source is invalid, even though it looks like it might be OK from the above):
2012-09-24 11:18:32.231 QR vCard[220:907] ABAddressBookAddRecord error = The operation couldn’t be completed. (ABAddressBookErrorDomain error 1.)
I thought I could get all the sources and then pick one, but the following returns none at all:
CFArrayRef allSources = ABAddressBookCopyArrayOfAllSources (_addressBook);
NSLog(#"2 - allSources = %#", allSources);
AB: Could not compile statement for query (ABCCopyArrayOfAllInstancesOfClassInSourceMatchingProperties):
SELECT ROWID, Name, ExternalIdentifier, Type, ConstraintsPath, ExternalModificationTag, ExternalSyncTag, AccountID, Enabled, SyncData, MeIdentifier, Capabilities FROM ABStore WHERE Enabled = ?;
2012-09-24 10:58:09.908 QR vCard[177:907] 2 - allSources = ()
I had the same issue and I could not get the Allow Access To Contacts alert to popup.
The Answer was posted by Kyle here:
https://stackoverflow.com/a/12648938/480415
// 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
});
}
else if (ABAddressBookGetAuthorizationStatus() == kABAuthorizationStatusAuthorized) {
// The user has previously given access, add the contact
}
else {
// The user has previously denied access
// Send an alert telling user to change privacy setting in settings app
}
This log message is an indication that your app is not (maybe not yet) allowed to access Contacts. iOS 6 gives users the possibility to deny apps the permission to access the address book.
The message disappears once the user has allowed your app access to Contacts - either via the pop up dialog, or by going to Settings -> Privacy -> Contacts.
For more infos on this topic, see WWDC 2012 session 710 "Privacy support in iOS and OS X".
If you got here from Google and you're using iOS's new CNContactStore framework, and getting these errors, read on:
I thought it'd be cleaner to make my CNContactStore a member variable that was initialized with the class instance:
class foo {
var contactStore = CNContactStore()
func findByIdentifier(identifier: String) -> CNContact {
let contact = try self.contactStore.unifiedContactWithIdentifier(identifier...
return contact
}
}
After I called this about fifty times, it started erroring out with
AB: Could not compile statement for query (ABCCopyArrayOfAllInstancesOfClassInSourceMatchingProperties)
I tried rate-limiting my calls, but that didn't help. It turned out that instantiating a new CNContactStore for every call had zero performance ramifications and completely solved the problem for me:
class foo {
func findByIdentifier(identifier: String) -> CNContact {
let contactStore = CNContactStore()
let contact = try contactStore.unifiedContactWithIdentifier(identifier...
return contact
}
}
Hope this helps!

Resources