38

Some beta-users of my upcoming app are reporting that the list of contacts contain a lot of duplicate records. I'm using the result from ABAddressBookCopyArrayOfAllPeople as the data source for my customized table view of contacts, and it baffles me that the results are different from the iPhone's 'Contacts' app.

When looking more closely at the Contacts app, it seems that the duplicates originate from entries with "Linked Cards". The screenshots below have been obfuscated a bit, but as you see in my app on the far right, "Celine" shows up twice, while in the Contacts app on the left there's only one "Celine". If you click the row of that single contact, you get a "Unified Info" card with two "Linked Cards" (as shown in the center, I didn't use Celine's contact details because they didn't fit on one screenshot):

Screenshot

The issues around "Linked Cards" have quite a few topics on Apple's forums for end users, but apart from the fact that many point to a 404 support page, I can't realistically go around fixing all of my app's users' address books. I would much rather like to deal with it elegantly and without bothering the user. To make matters worse, it seems I'm not the only one with this issue, since WhatsApp is showing the same list containing duplicate contacts.

Just to be clear about the origins of the duplicate contacts, I'm not storing, caching or otherwise trying to be smart about the array ABAddressBookCopyArrayOfAllPeople returns. So the duplicate records come directly from the API call.

Does anyone know how to deal with or detect these linked cards, preventing duplicate records from showing up? Apple's Contacts app does it, how can the rest of us do so too?

UPDATE: I wrote a library and put it on Cocoapods to solve the issue at hand. See my answer below

epologee
  • 10,851
  • 9
  • 64
  • 102
  • 1
    I think this is a bigger problem in iOS6, with the Facebook contacts. But since I am back in iOS5 i can't verify if it's the same.. – Jankeesvw Jul 07 '12 at 11:13
  • Facebook probably adds linked cards too... Then someone must have thought about the proper way to show a list, if it's not ABAddressBookCopyArrayOfAllPeople – epologee Jul 11 '12 at 07:24

5 Answers5

35

One method would be to only retrieve the contacts from the default address book source:

ABAddressBookRef addressBook = ABAddressBookCreate();
NSArray *people = (__bridge NSArray *)ABAddressBookCopyArrayOfAllPeopleInSource(addressBook, ABAddressBookCopyDefaultSource(addressBook));

But that is lame, right? It targets the on-device address book, but not extra contacts that might be in Exchange or other fancy syncing address books.

So here's the solution you're looking for:

  1. Iterate through the ABRecord references
  2. Grab each respective "linked references" (using ABPersonCopyArrayOfAllLinkedPeople)
  3. Bundle them in an NSSet (so that the grouping can be uniquely identified)
  4. Add that NSSet to another NSSet
  5. Profit?

You now have an NSSet containing NSSets of linked ABRecord objects. The overarching NSSet will have the same count as the number of contacts in your "Contacts" app.

Example code:

NSMutableSet *unifiedRecordsSet = [NSMutableSet set];

ABAddressBookRef addressBook = ABAddressBookCreate();
CFArrayRef records = ABAddressBookCopyArrayOfAllPeople(addressBook);
for (CFIndex i = 0; i < CFArrayGetCount(records); i++)
{
    NSMutableSet *contactSet = [NSMutableSet set];

    ABRecordRef record = CFArrayGetValueAtIndex(records, i);
    [contactSet addObject:(__bridge id)record];

    NSArray *linkedRecordsArray = (__bridge NSArray *)ABPersonCopyArrayOfAllLinkedPeople(record);
    [contactSet addObjectsFromArray:linkedRecordsArray];

    // Your own custom "unified record" class (or just an NSSet!)
    DAUnifiedRecord *unifiedRecord = [[DAUnifiedRecord alloc] initWithRecords:contactSet];

    [unifiedRecordsSet addObject:unifiedRecord];
    CFRelease(record);
}

CFRelease(records);
CFRelease(addressBook);

_unifiedRecords = [unifiedRecordsSet allObjects];
Daniel Amitay
  • 6,679
  • 7
  • 34
  • 43
  • It's lame that Apple doesn't provide this for us, but your unified address book solves the problem perfectly! I'll leave the question open in case anyone closer to Apple wants to weigh in, but I think the +100 are soon yours :) Thanks! – epologee Jul 14 '12 at 11:16
  • Daniel, I wonder why you removed the DAUnifiedAddressBook repository from GitHub. Sounds like something useful. – Leo Natan Sep 12 '12 at 16:55
  • =/ It was a stop-gap measure that I hadn't fully developed. In reality it simply had a "generateUnifiedContacts" call that performed the steps above. I had plans on developing it further, but in the end it wouldn't have been much superior to doing the above yourself and then handling the references as desired. – Daniel Amitay Sep 12 '12 at 18:16
  • BTW, you should not `CFRelease` the `record`, as you don't own it. But you should `CFRelease` the `linkedRecordsArray` (or, easier, transfer ownership to ARC with `__bridge_transfer` or `CFBridgingRelease`). – Rob Aug 19 '15 at 21:54
  • How can i get duplicate contact from address book? I had tried above solution but i could not get duplicate contact. – Parthpatel1105 Apr 21 '16 at 06:36
  • After using above code i could not get email address and phone numbers array from by using ABRecordRef. – Parthpatel1105 May 12 '16 at 09:23
8

I've been using ABPersonCopyArrayOfAllLinkedPeople() in my app for some time now. Unfortunately, I've just discovered that it doesn't always do the right thing. For example, if you have two contacts that have the same name but one has the "isPerson" flag set and the other does not, the above function won't consider them "linked". Why is this an issue? Because Gmail(exchange) sources don't support this boolean flag. If you try to save it as false, it will fail, and the contact you saved in it will come back on the next run of your app as unlinked from the contact you saved in iCload (CardDAV).

Similar situation with social services: Gmail doesn't support them and the function above will see two contacts with the same names as different if one has a facebook account and one does not.

I'm switching over to my own name-and-source-recordID-only algorithm for determining whether two contact records should be displayed as a single contact. More work but there's a silver lining: ABPersonCopyArrayOfAllLinkedPeople() is butt-slow.

  • 1
    Welcome to Stack Overflow. There are two previous answers, one that was already up-voted quite a lot, and another from the original poster identifying what they actually did to resolve the problem. I'm not sure if what you're proposing is significantly different from what he's placed for others to use in a Github download, but I'm not sure that your answer is really helping since it is not clear that others can get at your solution code. I won't down-vote you for this, but please do consider whether an answer you give is providing new information when it is an older question like this. – Jonathan Leffler Oct 07 '12 at 04:35
  • 3
    I think Christopher's answer was mainly intended to provide some background information on the possible gaps in ABPersonCopyArrayOfAllLinkedPeople() implementation. I haven't seen that information elsewhere, so I believe it could be useful to myself or others. – Peter Johnson Oct 25 '12 at 17:47
  • 1
    I think this is a highly informative answer, and includes information that would be otherwise difficult to obtain that is pertinent to the question and previously given answers. Upvoted. – Tim Mar 04 '15 at 02:17
  • This is still useful information, as Apples documentation still explains nothing of the logic of how it decides to unify contacts. Any clues are welcome – Peter Johnson Feb 25 '21 at 15:11
5

The approach that @Daniel Amitay provided contained nuggets of great value, but unfortunately the code is not ready for use. Having a good search on the contacts is crucial to my and many apps, so I spent quite a bit of time getting this right, while on the side also addressing the issue of iOS 5 and 6 compatible address book access (handling user access via blocks). It solves both the many linked cards due to incorrectly synched sources and the cards from the newly added Facebook integration.

The library I wrote uses an in-memory (optionally on-disk) Core Data store to cache the address book record ID's, providing an easy background-threaded search algorithm that returns unified address book cards.

The source is available on a github repository of mine, which is a CocoaPods pod:

pod 'EEEUnifiedAddressBook'
epologee
  • 10,851
  • 9
  • 64
  • 102
5

With the new iOS 9 Contacts Framework you can finally have your unified contacts.

I show you two examples:

1) Using fast enumeration

//Initializing the contact store:
CNContactStore* contactStore = [CNContactStore new];
if (!contactStore) {
    NSLog(@"Contact store is nil. Maybe you don't have the permission?");
    return;
}

//Which contact keys (properties) do you want? I want them all!
NSArray* contactKeys = @[ 
    CNContactNamePrefixKey, CNContactGivenNameKey, CNContactMiddleNameKey, CNContactFamilyNameKey, CNContactPreviousFamilyNameKey, CNContactNameSuffixKey, CNContactNicknameKey, CNContactPhoneticGivenNameKey, CNContactPhoneticMiddleNameKey, CNContactPhoneticFamilyNameKey, CNContactOrganizationNameKey, CNContactDepartmentNameKey, CNContactJobTitleKey, CNContactBirthdayKey, CNContactNonGregorianBirthdayKey, CNContactNoteKey, CNContactImageDataKey, CNContactThumbnailImageDataKey, CNContactImageDataAvailableKey, CNContactTypeKey, CNContactPhoneNumbersKey, CNContactEmailAddressesKey, CNContactPostalAddressesKey, CNContactDatesKey, CNContactUrlAddressesKey, CNContactRelationsKey, CNContactSocialProfilesKey, CNContactInstantMessageAddressesKey
];

CNContactFetchRequest* fetchRequest = [[CNContactFetchRequest alloc] initWithKeysToFetch:contactKeys];
[fetchRequest setUnifyResults:YES]; //It seems that YES is the default value
NSError* error = nil;
__block NSInteger counter = 0;

And here i loop through all unified contacts using fast enumeration:

BOOL success = [contactStore enumerateContactsWithFetchRequest:fetchRequest
                                                         error:&error
                                                    usingBlock:^(CNContact* __nonnull contact, BOOL* __nonnull stop) {
                                                        NSLog(@"Unified contact: %@", contact);
                                                        counter++;
                                                    }];
if (success) {
    NSLog(@"Successfully fetched %ld contacts", counter);
}
else {
    NSLog(@"Error while fetching contacts: %@", error);
}

2) Using unifiedContactsMatchingPredicate API:

// Contacts store initialized ...
NSArray * unifiedContacts = [contactStore unifiedContactsMatchingPredicate:nil keysToFetch:contactKeys error:&error]; // Replace the predicate with your filter.

P.S You maybe also be interested at this new API of CNContact.h:

/*! Returns YES if the receiver was fetched as a unified contact and includes the contact having contactIdentifier in its unification */
- (BOOL)isUnifiedWithContactWithIdentifier:(NSString*)contactIdentifier;
andreacipriani
  • 2,450
  • 23
  • 23
  • Its a shame it still doesnt let you tell iOS that two contacts should be linked (although users can do this). This means if I generate a duplicate in a different address book, so I can add it to a group, I end up with two entries in the users' Contacts app. – Peter Johnson Feb 25 '21 at 15:14
0

I'm getting all sources ABAddressBookCopyArrayOfAllSources, moving the default one ABAddressBookCopyDefaultSource to the first position, then iterate through them and getting all people from source ABAddressBookCopyArrayOfAllPeopleInSource skipping ones I've seen linked before, then getting linked people on each ABPersonCopyArrayOfAllLinkedPeople.

Rob Glassey
  • 2,227
  • 18
  • 21
Hafthor
  • 15,081
  • 9
  • 54
  • 62