Skip to content

Instantly share code, notes, and snippets.

@quintonpryce
Created November 2, 2020 16:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save quintonpryce/d69499bf40bdef208bd35d00aba188db to your computer and use it in GitHub Desktop.
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
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