Как получить доступ к контактам iOS в пакетном режиме?

Я пытаюсь реализовать средство чтения контактов в Xamarin.iOS, которое пытается перебрать контакты iOS во всех контейнерах CNContactStore. Вместо того, чтобы загружать все контакты в память, мне нужно перебирать пакет результатов по пакетам (контакты с пейджингом). Однако все примеры, которые я видел в SO, сначала загружают почти все контакты в память.

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

Собственная документация Apple гласит

When fetching all contacts and caching the results, first fetch all contacts identifiers, then fetch batches of detailed contacts by identifiers as required

Я смог сделать это легко для Android, используя подход на основе курсора, доступный в его SDK. Это вообще возможно для iOS? Если нет, то как мы можем справиться с большим количеством контактов (например, что-то выше 2000 и т. Д.). Я не против примеров в быстром. Я должен быть в состоянии преобразовать их в Xamarin.

Заранее спасибо.

1 ответ

Вот подход, который я выбрал, учитывая, что мои требования не позволяют сохранять контакты, только удерживая их в активной памяти. Не сказать, что это правильный подход, но сначала выбор всех идентификаторов, а затем ленивый выбор всех ключей для определенного контакта при необходимости улучшил производительность. Это также избегает выполнения поиска, когда контакт не существует. Я также попытался использовать NSCache вместо словаря, но столкнулся с проблемой, когда мне нужно было перебрать кеш.

Я обрезал функции, которые не имеют отношения к теме, но, надеюсь, все еще передают подход.

import Contacts

extension CNContactStore {

    // Used to seed a Contact Cache with all identifiers
    func getAllIdentifiers() -> [String: CNContact]{

        // keys to fetch from store
        let minimumKeys: [CNKeyDescriptor] = [
            CNContactPhoneNumbersKey as CNKeyDescriptor,
            CNContactIdentifierKey as CNKeyDescriptor
        ]

        // contact request
        let request = CNContactFetchRequest(keysToFetch: minimumKeys)

        // dictionary to hold results, phone number as key
        var results: [String: CNContact] = [:]

        do {
            try enumerateContacts(with: request) { contact, stop in

                for phone in contact.phoneNumbers {
                    let phoneNumberString = phone.value.stringValue
                    results[phoneNumberString] = contact
                }
            }
        } catch let enumerateError {
            print(enumerateError.localizedDescription)
        }

        return results
    }

    // retreive a contact using an identifier
    // fetch keys lists any CNContact Keys you need
    func get(withIdentifier identifier: String, keysToFetch: [CNKeyDescriptor]) -> CNContact? {

        var result: CNContact?
        do {
            result = try unifiedContact(withIdentifier: identifier, keysToFetch: keysToFetch)
        } catch {
            print(error)
        }

        return result
    }
}

final class ContactsCache {

    static let shared = ContactsCache()

    private var cache : [String : ContactCacheItem] = [:]

    init() {

        self.initializeCache()  // calls CNContactStore().getAllIdentifiers() and loads into cache

        NotificationCenter.default.addObserver(self, selector: #selector(contactsAppUpdated), name: .CNContactStoreDidChange, object: nil)
    }

    private func initializeCache() {

        DispatchQueue.global(qos: .background).async {

            let seed = CNContactStore.getAllIdentifiers()

            for (number, contact) in seed{

                let item = ContactCacheItem.init(contact: contact, phoneNumber: number )
                self.cache[number] = item
            }
        }
    }

    // if the contact is in cache, return immediately, else fetch and execute completion when finished. This is bit wonky to both return value and execute completion, but goal was to reduce visible cell async update as much as possible
    public func contact(for phoneNumber: String, completion: @escaping (CNContact?) -> Void) -> CNContact?{

        if !initialized {   // the cache has not finished seeding, queue request

            queueRequest(phoneNumber: phoneNumber, completion: completion)  // save request to be executed as soon as seeding completes
            return nil
        }

        // item is in cache
        if let existingItem = getCachedContact(for: phoneNumber) {

            // is it being looked up
            if existingItem.lookupInProgress(){
                existingItem.addCompletion(completion: completion)
            }
            // is it stale or has it never been looked up
            else if existingItem.shouldPerformLookup(){

                existingItem.addCompletion(completion: completion)
                refreshCacheItem( existingItem )
            }
            // its current, return it
            return existingItem.contact
        }

        // item is not in cache
        completion(nil)
        return nil
    }

    private func getCachedContact(for number: String) -> ContactCacheItem?  {
        return self.cache.first(where: { (key, _) in key.contains( number) })?.value
    }


    // during the async initialize/seeding of the cache, requests may come in from app, so they are temporarily 'queued'
    private func queueRequest(phoneNumber: String, completion: @escaping (CNContact?) -> Void){..}
    // upon async initialize/seeding completion, queued requests can be executed
    private func executeQueuedRequests() {..}
    // if app receives notification of update to user contacts, refresh cache
    @objc func contactsAppUpdated(_ notification: Notification) {..}
    // if a contact has gone stale or never been fetched, perform the fetch
    private func refreshCacheItem(_ item: ContactCacheItem){..}
    // if app receives memory warning, dump data
    func clearCaches() {..}
}

class ContactCacheItem : NSObject {

    var contact: CNContact? = nil
    var lookupAttempted : Date?  // used to determine when last lookup started
    var lookupCompleted : Date?  // used to determien when last successful looup completed
    var phoneNumber: String     //the number used to look this item up
    private var callBacks = ContactLookupCompletion()  //used to keep completion blocks for lookups in progress, in case multilpe callers want the same contact info

    init(contact: CNContact?, phoneNumber: String){..}
    func updateContact(contact: CNContact?){..}  // when a contact is fetched from store, update it here
    func lookupInProgress() -> Bool {..}
    func shouldPerformLookup() -> Bool {..}
    func hasCallBacks() -> Bool {..}
    func addCompletion(completion: @escaping (CNContact?) -> Void){..}
}
Другие вопросы по тегам