Работа с дублирующимися контактами из-за связанных карт в 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 или других модных адресных книгах синхронизации.
Итак, вот решение, которое вы ищете:
- Итерация по ссылкам ABRecord
- Захватите каждую соответствующую "связанную ссылку" (используя
ABPersonCopyArrayOfAllLinkedPeople
) - Объедините их в NSSet (чтобы можно было однозначно идентифицировать группу)
- Добавьте этот NSSet к другому NSSet
- Прибыль?
Теперь у вас есть 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
,