Dealing with duplicate contacts due to linked cards in iOS' Address Book API

为君一笑 提交于 2019-11-28 15:43:57

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];

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.

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'

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;
Hafthor

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!