Created
November 2, 2020 16:38
-
-
Save quintonpryce/d69499bf40bdef208bd35d00aba188db to your computer and use it in GitHub Desktop.
Retrieving a contact's phone number from a name in an INPerson object
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Contacts | |
import Intents | |
protocol ContactStoring { | |
func enumerateContacts(with fetchRequest: CNContactFetchRequest, usingBlock block: @escaping (CNContact, UnsafeMutablePointer<ObjCBool>) -> Void) throws | |
} | |
extension CNContactStore: ContactStoring {} | |
class IntentPersonProvider { | |
private let contactStore: ContactStoring | |
/// 5 is the SiriKit standard number for asking the user to disambiguate options. | |
private let maxNumberOfOptions = 5 | |
init(contactStore: ContactStoring = CNContactStore()) { | |
self.contactStore = contactStore | |
} | |
/// Returns a list of contacts whose first or last name matches the person's display name. If that list has more than one entry, it is filtered by the exact name. | |
/// | |
/// Returns a maximum of 5 persons. | |
func getPersonsInContacts(_ person: INPerson) -> [INPerson] { | |
var contacts: [CNContact] = [] | |
let keys: [CNKeyDescriptor] = [ | |
CNContactGivenNameKey as CNKeyDescriptor, | |
CNContactFamilyNameKey as CNKeyDescriptor, | |
CNContactPhoneNumbersKey as CNKeyDescriptor | |
] | |
do { | |
try contactStore.enumerateContacts(with: CNContactFetchRequest(keysToFetch: keys)) { contact, _ -> Void in | |
guard !contact.phoneNumbers.isEmpty else { return } | |
// We're very accepting of contacts, we will filter them out later if we need to shorten the matched list. | |
let personDisplayName = person.displayName.lowercased() | |
let contactFirstName = contact.givenName.lowercased() | |
let contactLastName = contact.familyName.lowercased() | |
if personDisplayName.containsIgnoringCase(contactFirstName) || | |
personDisplayName.containsIgnoringCase(contactLastName) { | |
contacts.append(contact) | |
} | |
} | |
if contacts.count == 0 { | |
print("No matching contacts...") | |
} | |
} catch { | |
assertionFailure("Unable to fetch contacts.") | |
} | |
contacts = filterContactsByExactName(contacts, for: person.displayName) | |
let persons = createPersonsFromContacts(contacts, for: person.displayName) | |
let personsWithoutDuplicates = removeDuplicateNumbers(persons) | |
return Array(personsWithoutDuplicates.prefix(maxNumberOfOptions)) | |
} | |
/// If we matched more than 1 contact we can try to thin the list by exact name. | |
private func filterContactsByExactName(_ contacts: [CNContact], for displayName: String) -> [CNContact] { | |
guard contacts.count >= 1 else { return contacts } | |
// If we have more than one contact that matches both first and last name we use those contacts. | |
let filteredContacts = contacts.filter { | |
displayName.containsIgnoringCase($0.givenName) && | |
displayName.containsIgnoringCase($0.familyName) | |
} | |
// Guard that our filtered list has at least one contact. | |
guard filteredContacts.count >= 1 else { | |
return contacts | |
} | |
// If our filtered list had no contacts. | |
return filteredContacts | |
} | |
private func removeDuplicateNumbers(_ persons: [INPerson]) -> [INPerson] { | |
let removedDuplicatePhoneNumbers = persons.reduce([]) { personsWithoutDuplicates, nextPerson -> [INPerson] in | |
// If the next person's handle is nil don't add the next person. | |
guard let nextPersonPhoneNumber = nextPerson.personHandle?.value else { | |
return personsWithoutDuplicates | |
} | |
// Get all phone numbers for personsWithoutDuplicates. | |
let phoneNumbersWithoutDuplicates = getPhoneNumbers(personsWithoutDuplicates) | |
// If phoneNumbersWithoutDuplicates contains the nextPerson's phone number do not append it to the reduce. | |
guard !phoneNumbersWithoutDuplicates.contains(nextPersonPhoneNumber) else { | |
return personsWithoutDuplicates | |
} | |
return personsWithoutDuplicates + [nextPerson] | |
} | |
return removedDuplicatePhoneNumbers | |
} | |
private func getPhoneNumbers(_ persons: [INPerson]) -> [String] { | |
persons.compactMap { $0.personHandle?.value } | |
} | |
private func createPersonsFromContacts(_ contacts: [CNContact], for displayName: String) -> [INPerson] { | |
let persons = contacts.flatMap { contact -> [INPerson] in | |
let personHandles: [INPersonHandle] = contact.phoneNumbers.compactMap { | |
INPersonHandle(value: $0.value.stringValue, type: .phoneNumber) | |
} | |
let persons = personHandles.compactMap { personHandle -> INPerson in | |
INPerson( | |
personHandle: personHandle, | |
nameComponents: nil, | |
displayName: displayName, | |
image: nil, contactIdentifier: nil, customIdentifier: nil | |
) | |
} | |
return persons | |
} | |
return persons | |
} | |
} | |
private extension String { | |
func containsIgnoringCase(_ string: String) -> Bool { | |
lowercased().contains(string.lowercased()) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment