Работа с дублирующимися контактами из-за связанных карт в API адресной книги iOS

Некоторые бета-пользователи моего будущего приложения сообщают, что список контактов содержит много повторяющихся записей. Я использую результат из ABAddressBookCopyArrayOfAllPeople в качестве источника данных для моего настроенного представления таблицы контактов, и это сбивает с толку меня, что результаты отличаются от приложения iPhone "Контакты".

Если присмотреться к приложению "Контакты", кажется, что дубликаты происходят из записей с "Связанными картами". Снимки экрана ниже были немного запутаны, но, как вы видите в моем приложении справа, "Селин" появляется дважды, в то время как в приложении "Контакты" слева только один "Селин". Если щелкнуть строку этого единственного контакта, вы получите карточку "Единая информация" с двумя "Связанными карточками" (как показано в центре, я не использовал контактную информацию Селин, потому что они не помещались на одном снимке экрана):

Скриншот

Проблемы, связанные с "Связанными картами", имеют довольно много тем на форумах Apple для конечных пользователей, но, помимо того, что многие указывают на страницу поддержки 404, я не могу реально исправить все адресные книги всех пользователей моего приложения. Я бы предпочел иметь дело с этим элегантно и без беспокойства пользователя. Что еще хуже, кажется, что я не единственный, кто сталкивается с этой проблемой, поскольку WhatsApp показывает тот же список, содержащий дубликаты контактов.

Просто чтобы быть ясным в отношении происхождения дублированных контактов, я не храню, не кэширую и не пытаюсь быть умным в отношении массива. ABAddressBookCopyArrayOfAllPeople возвращается. Таким образом, дубликаты записей поступают непосредственно из вызова API.

Кто-нибудь знает, как обращаться с этими связанными карточками или обнаруживать их, не допуская появления дублированных записей? Приложение "Контакты" от Apple делает это, как остальные тоже могут это сделать?

ОБНОВЛЕНИЕ: я написал библиотеку и поместил ее в Cocoapods, чтобы решить проблему под рукой. Смотрите мой ответ ниже

5 ответов

Решение

Подход, предложенный @Daniel Amitay, содержал очень ценные самородки, но, к сожалению, код не готов к использованию. Хороший поиск по контактам имеет решающее значение для моего и многих приложений, поэтому я потратил немало времени на то, чтобы сделать это правильно, и в то же время занимался вопросами доступа к адресной книге, совместимой с iOS 5 и 6 (обработка доступа пользователей через блоки). Он решает как много связанных карт из-за неправильно синхронизированных источников, так и карты из недавно добавленной интеграции Facebook.

Библиотека, которую я написал, использует базовое хранилище данных в памяти (необязательно на диске) для кэширования идентификаторов записей адресной книги, предоставляя простой фоновый алгоритм поиска, который возвращает унифицированные карточки адресной книги.

Источник доступен в моем github-репозитории, который является модулем CocoaPods:

pod 'EEEUnifiedAddressBook'

Одним из методов будет получение контактов только из источника адресной книги по умолчанию:

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

Но это хромает, верно? Он предназначен для адресной книги на устройстве, но не для дополнительных контактов, которые могут быть в Exchange или других модных адресных книгах синхронизации.

Итак, вот решение, которое вы ищете:

  1. Итерация по ссылкам ABRecord
  2. Захватите каждую соответствующую "связанную ссылку" (используя ABPersonCopyArrayOfAllLinkedPeople)
  3. Объедините их в NSSet (чтобы можно было однозначно идентифицировать группу)
  4. Добавьте этот NSSet к другому NSSet
  5. Прибыль?

Теперь у вас есть NSSet, содержащий NSSets связанных объектов ABRecord. Общий NSSet будет иметь то же количество, что и количество контактов в приложении "Контакты".

Пример кода:

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

Я уже давно использую ABPersonCopyArrayOfAllLinkedPeople() в своем приложении. К сожалению, я только что обнаружил, что это не всегда делает правильные вещи. Например, если у вас есть два контакта с одинаковыми именами, но у одного из них установлен флаг "isPerson", а у другого - нет, вышеуказанная функция не будет считать их "связанными". Почему это проблема? Потому что источники Gmail(exchange) не поддерживают этот логический флаг. Если вы попытаетесь сохранить его как false, произойдет сбой, и контакт, который вы сохранили в нем, вернется при следующем запуске вашего приложения как несвязанный с контактом, который вы сохранили в iCload (CardDAV).

Аналогичная ситуация с социальными службами: Gmail не поддерживает их, и приведенная выше функция увидит два контакта с одинаковыми именами как разные, если у одного есть учетная запись на Facebook, а у другого - нет.

Я переключаюсь на свой собственный алгоритм "только для имени и источника-записи-идентификатора", чтобы определить, должны ли две записи контактов отображаться как один контакт. Больше работы, но есть серебряная подкладка: ABPersonCopyArrayOfAllLinkedPeople() медленный.

С новой iOS 9 Contacts Framework вы можете, наконец, получить ваши унифицированные контакты.

Я покажу вам два примера:

1) Использование быстрого перечисления

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

И здесь я перебираю все объединенные контакты, используя быстрое перечисление:

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) Использование unifiedContactsMatchingPredicate API:

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

PS Возможно, вас также заинтересует этот новый API 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;

Я получаю все источники ABAddressBookCopyArrayOfAllSources, перемещая один по умолчанию ABAddressBookCopyDefaultSource на первую позицию, затем переберите их и получите всех людей из источника ABAddressBookCopyArrayOfAllPeopleInSource пропуская те, которые я видел ранее, затем связывая людей на каждом ABPersonCopyArrayOfAllLinkedPeople,

Другие вопросы по тегам