Traitement des contacts en double dus aux cartes liées dans l’API du carnet d’adresses iOS

Certains utilisateurs bêta de ma prochaine application signalent que la liste des contacts contient de nombreux enregistrements en double. J’utilise le résultat de ABAddressBookCopyArrayOfAllPeople comme source de données pour mon affichage de table personnalisé des contacts, et cela m’étonne que les résultats diffèrent de l’application Contacts de l’iPhone.

En regardant de plus près l’application Contacts, il semble que les doublons proviennent d’entrées avec “Cartes liées”. Les captures d’écran ci-dessous ont été un peu obscurcies, mais comme vous le voyez dans mon application à l’extrême droite, “Celine” apparaît deux fois, tandis que dans l’application Contacts à gauche, il n’y a qu’une seule “Celine”. Si vous cliquez sur la ligne de ce contact unique, vous obtenez une carte “Unified Info” avec deux “cartes liées” (comme indiqué au centre, je n’ai pas utilisé les coordonnées de Celine car elles ne tiennent pas sur une capture d’écran) :

Capture d'écran

Les problèmes entourant les “cartes liées” ont plusieurs sujets sur les forums d’ Apple pour les utilisateurs finaux, mais mis à part le fait que beaucoup de sites proposent une page 404 , je ne peux pas réparer tous les carnets d’adresses des utilisateurs. J’aimerais beaucoup en traiter avec élégance et sans déranger l’utilisateur. Pour aggraver les choses, il semble que je ne suis pas le seul à avoir ce problème, puisque WhatsApp affiche la même liste contenant des contacts en double .

Juste pour être clair sur l’origine des contacts en double, je ne stocke pas, ne cache pas ou ABAddressBookCopyArrayOfAllPeople pas d’être intelligent sur le tableau que ABAddressBookCopyArrayOfAllPeople renvoie. Les enregistrements en double proviennent donc directement de l’appel de l’API.

Est-ce que quelqu’un sait comment traiter ou détecter ces cartes liées, empêchant l’affichage de doublons? L’application Contacts d’Apple le fait, comment pouvons-nous le faire aussi?

MISE À JOUR: J’ai écrit une bibliothèque et l’a mise sur Cocoapods pour résoudre le problème. Voir ma réponse ci-dessous

Une méthode serait de ne récupérer que les contacts de la source de carnet d’adresses par défaut:

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

Mais c’est boiteux, non? Il cible le carnet d’adresses sur l’appareil, mais pas les contacts supplémentaires pouvant se trouver dans Exchange ou dans d’autres carnets d’adresses de synchronisation sophistiqués.

Alors, voici la solution que vous recherchez:

  1. Parcourez les références ABRecord
  2. Saisissez chacune des “références liées” respectives (en utilisant ABPersonCopyArrayOfAllLinkedPeople )
  3. Les regrouper dans un NSSet (afin que le regroupement puisse être identifié de manière unique)
  4. Ajouter ce NSSet à un autre NSSet
  5. Profit?

Vous disposez maintenant d’un NSSet contenant des NSSets d’objects ABRecord liés. Le NSSet global aura le même nombre que le nombre de contacts dans votre application “Contacts”.

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

J’utilise ABPersonCopyArrayOfAllLinkedPeople () depuis quelque temps déjà dans mon application. Malheureusement, je viens de découvrir que cela ne fait pas toujours ce qu’il faut. Par exemple, si vous avez deux contacts qui portent le même nom, mais que l’un d’eux dispose de l’indicateur “isPerson” et que l’autre ne l’est pas, la fonction ci-dessus ne les considérera pas comme “liés”. Pourquoi est-ce un problème? Parce que les sources Gmail (échange) ne prennent pas en charge cet indicateur booléen. Si vous essayez de l’enregistrer en tant que faux, il échouera et le contact que vous avez enregistré y reviendra lors de la prochaine exécution de votre application, sans lien avec le contact enregistré dans iCload (CardDAV).

Une situation similaire avec les services sociaux: Gmail ne les prend pas en charge et la fonction ci-dessus verra deux contacts avec les mêmes noms différents si l’un a un compte Facebook et que l’autre ne l’a pas.

Je passe à mon propre algorithme, nom-et-source-enregistrement-ID uniquement, pour déterminer si deux enregistrements de contact doivent être affichés en tant que contact unique. Plus de travail, mais il y a un côté positif: ABPersonCopyArrayOfAllLinkedPeople () est lent.

L’approche fournie par @Daniel Amitay contenait des pépites de grande valeur, mais malheureusement, le code n’est pas prêt à être utilisé. Avoir une bonne recherche sur les contacts est crucial pour moi et pour de nombreuses applications, alors j’ai passé pas mal de temps à bien faire les choses, tout en abordant le problème de l’access aux carnets d’adresses compatibles iOS 5 et 6 (gérer les access utilisateurs via des blocs) ). Il résout à la fois les nombreuses cartes liées en raison de sources mal synchronisées et les cartes de l’intégration Facebook nouvellement ajoutée.

La bibliothèque que j’ai écrite utilise un stockage Core Data en mémoire (éventuellement sur disque) pour mettre en cache les ID d’enregistrement du carnet d’adresses, fournissant un algorithme de recherche par thread en arrière-plan simple qui renvoie des cartes de carnet d’adresses unifiées.

La source est disponible sur mon repository github , qui est un conteneur CocoaPods :

 pod 'EEEUnifiedAddressBook' 

Avec le nouveau framework iOS 9 Contacts, vous pouvez enfin avoir vos contacts unifiés.

Je vous montre deux exemples:

1) En utilisant le dénombrement rapide

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

Et ici, je passe en revue tous les contacts unifiés en utilisant une énumération rapide:

 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) Utilisation de l’API unifiedContactsMatchingPredicate :

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

PS Vous êtes peut-être également intéressé par cette nouvelle API de CNContact.h :

 /*! Returns YES if the receiver was fetched as a unified contact and includes the contact having contactIdentifier in its unification */ - (BOOL)isUnifiedWithContactWithIdentifier:(NSSsortingng*)contactIdentifier; 

ABAddressBookCopyArrayOfAllSources toutes les sources ABAddressBookCopyArrayOfAllSources , en déplaçant la valeur par défaut ABAddressBookCopyDefaultSource à la première position, puis en les parcourant et en faisant passer toutes les personnes de la source ABAddressBookCopyArrayOfAllPeopleInSource celles qui ont déjà été liées.