这是我采取的方法,因为我的要求不允许持久联系,只能保持活动记忆。并不是说这是正确的方法,而是首先获取所有标识符,然后根据需要延迟获取特定联系人的所有密钥,确实提高了性能。它还避免在联系人不存在时执行查找。我也尝试使用 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){..}
}