The mysterious world of Address Book and Contacts Store in iOS

Last couple of month I’ve been working on the side project app which tries to replace the native iOS contacts application.

In the design stage I was pretty excited because I hadn’t had a chance to work heavily with AddressBook framework before. And the second reason was because Apple had recently released Contacts framework in order to replace old fashioned one.

Because of the app supports iOS version 8.0 and higher, I had to create an adapter protocol to describe class that works with native contacts app data. That protocol describes simple methods:

- (void)requestAccessWithCallback:(ADContactsManagerAccessBlock)callback;
- (void)updateContact:(ADContact *)contact withCallback:(ADContactsManagerUpdateBlock)callback;
- (void)addContact:(ADContact *)contact withCallback:(ADContactsManagerAddBlock)callback;
- (void)deleteContact:(ADContact *)contact withCallback:(ADContactsManagerDeleteBlock)callback;
- (void)getContactsWithCallback:(ADContactsManagerGetBlock)callback;
- (void)getContactsCountWithCallback:(ADContactsManagerCountBlock)callback;

For iOS version prior to 9.0 I’ve created class to work with AddressBook framework:

@interface ADAddressBookFrameworkManager : NSObject <ADContactsManager>
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored “-Wdeprecated-declarations”
- (instancetype)initWithSystemAddressBook:(ABAddressBookRef)addressBook;
#pragma GCC diagnostic pop
@end

And another class to use with modern iOS version, that uses Contacts framework under the hood:

@interface ADContactsFrameworkManager : NSObject <ADContactsManager>
- (instancetype)initWithSystemContactsStore:(CNContactStore *)contactsStore;
@end

I’ve really enjoyed new Contacts framework, because working with objects was much easier, less buggy and faster to code rather then with C style methods in AddressBook framework. Things went worse when our QA team found really strange bug related to updateContact method.

Let’s go deeper With new Contacts framework logic to update contact object in native Contacts app is pretty simple unless you want to remove any property value e.x. organization name or job title. Regarding Apple’s documentation it’s easy:

@note To remove properties when saving a mutable contact, set string properties and array properties to empty values. Set other properties to nil.

So my way to do it was to fetch CNContact object to update in the beginning:

CNContactFetchRequest *request = [[CNContactFetchRequest alloc] initWithKeysToFetch:@[all keys that can be updated]];
request.predicate = [CNContact predicateForContactsWithIdentifiers:identifiers];
CNContact __block *foundContact = nil;
NSError *fetchError = nil;
[self.contactsStore enumerateContactsWithFetchRequest:request error:&fetchError usingBlock:^(CNContact *_Nonnull contact, BOOL *_Nonnull stop) {
foundContact = contact;
if (stop) {
*stop = YES;
}
}];

Create a mutable copy of the found instance to assign empty string for the string property we want to remove:

CNMutableContact *updatedFrameworkContact = [foundContact mutableCopy];
updatedFrameworkContact.jobTitle = @””;

And then we just need to save this update contact in the store:

CNSaveRequest *updateRequest = [CNSaveRequest new];
[updateRequest updateContact:updatedFrameworkContact];
NSError *error;
[self.contactsStore executeSaveRequest:request error:error];

And suddenly it returns mysterious error:

Error Domain=CNErrorDomain Code=500 “(null)” UserInfo={NSUnderlyingError=0x138066570 {Error Domain=ABAddressBookErrorDomain Code=0 “(null)”}}

Thanks Apple, this shows us how the error message can be ”useful”. I spent a lot of time to figure out the problem: looking on my code, googling, searching on stackoverflow — nothing. Probably I didn’t find anything because the framework was still young and there were not many questions and problems had been discussed. The problem becomes even more mystic because it only appears on device and only when update value to empty string.

Anyway after one day of researching I’ve unfortunately ended up to get back to AddressBook framework since it does not have such problem.

But it has another problem…

Things went well for a couple of weeks but then our QA team created a task that after succeeded sync the app needs to be restarted to apply changes. I started to check what can cause this issue went right to my ADAddressBookFrameworkManager class. After debugging I found unexpected behavior for me. If you update any person in native contacts app, current instance of ABAddressBookRef will return old data in ABRecordRef that represents updated contact.

Again I didn’t find anything similar on stackoverflow. I remember that I saw a method which the app can use to handle updates in ABAddressBookRef.

I implemented that external changes callback in my manager:

ref = ABAddressBookCreateWithOptions(NULL, &error);
ABAddressBookRegisterExternalChangeCallback(ref, addressBookChanged, (__bridge void *)(self));
void addressBookChanged(ABAddressBookRef addressBook, CFDictionaryRef info, void *context) {
}

Well that looks awesome and it even has something called info but unfortunately according the documentation:

The info argument may eventually contain information describing the change. Currently it will always be NULL.

Ok Apple very useful again. Eventually I came up with idea to create new instance of ABAddressBookRef when the manager receives that callback. This finally solved the issue and new instance returns up to date contacts information.

void addressBookChanged(ABAddressBookRef addressBook, CFDictionaryRef info, void *context) {
TUAddressBookFrameworkManager *manager = (__bridge
TUAddressBookFrameworkManager *)context;
manager.addressBook = ABAddressBookCreateWithOptions(NULL, NULL);
}

Conclusion

Even after that strange issue in Contacts framework, I really like that finally we can work with objects. Everything becomes much easier. And when Apple releases update with fix, I’ll definitely get back to it.

Also at the moment I started that project I didn’t find any third party library to wrap both Contacts and AddressBook frameworks and I developed the solution by myself. I think this also can be interesting for other developers and I’m going to release it in open source very soon. You can receive an update following me on Github.