skip to Main Content

I am accessing all Contacts with name and phone numbers. I am getting an array of CNContact with multiple phone numbers in single CNContact, which is expected.

func fetchContacts() {
    let store = CNContactStore()
    let keysToFetch = [
        CNContactFormatter.descriptorForRequiredKeys(for: .fullName),
        CNContactPhoneNumbersKey as any CNKeyDescriptor
    ]
    let fetchRequest = CNContactFetchRequest(keysToFetch: keysToFetch)
    var result: [CNContact] = []
    
    do {
        try store.enumerateContacts(with: fetchRequest) { contact, stop in
            if contact.phoneNumbers.count > 0 {
                result.append(contact)
            }
        }
        print(result)
    } catch let error {
        print("Fetching contacts failed: (error)")
    }
}

Output:

[
    Contact(fullName : "John Appleseed", phoneNumbers: ["888-555-5512", "888-555-1212"]),
    Contact(fullName : "Kate Bell",      phoneNumbers: ["(555) 564-8583", "(415) 555-3695"])
]

But what I want to achieve is an array of CNContact with single phone number.

[
    Contact(fullName : "John Appleseed", phoneNumber: "888-555-5512"),
    Contact(fullName : "John Appleseed", phoneNumber: "888-555-1212"),
    Contact(fullName : "Kate Bell",      phoneNumber: "(555) 564-8583"),
    Contact(fullName : "Kate Bell",      phoneNumber: "(415) 555-3695")
]

What NSPredictionNSPredicate will work in this case?

NOTE: Ignore UI error, already implemented permissions, want to achieve before reading contacts (fetch request) not after reading contacts.

2

Answers


  1. You can’t use NSPredicate to change the output of the query, it’s only meant for filtering the data. If you want a custom format for your output you are going to need a custom type

    For instance

    struct Contact {
        let name: String
        let phoneNumber: String
    }
    

    And then convert to that type in your loop

    var resultContact: [Contact] = []
    do {
        try store.enumerateContacts(with: fetchRequest) { contact, stop in
            if contact.phoneNumbers.count > 0 {
                contact.phoneNumbers.forEach { phoneNumber in
                    let phoneNumber = phoneNumber.value.stringValue
                    resultContact.append(Contact(name: contact.givenName + " " + contact.familyName, phoneNumber: phoneNumber))
                }
            }
        }
        print(resultContact)
    } catch let error {
        print("Fetching contacts failed: (error)")
    }
    
    Login or Signup to reply.
  2. Predicates are for filtering the fetched CNContact objects, to limit the results to those that match some criteria. For example, you might use a predicate to fetch only contacts with phone numbers:

    request.predicate = NSPredicate(format: "phoneNumbers.@count > 0")
    

    But it won’t “split” a single CNContact into multiple entries, one for each phone number. You have to do that yourself. You can use the pattern that Joakim outlined (+1). Or, personally, I might use a flatMap method that takes an array of return values and builds an array from that:

    extension CNContactStore {
        /// Flat map
        ///
        /// - Parameters:
        ///   - request: The `CNContactFetchRequest`.
        ///   - transform: A closure that returns an array of values to be appended to the final results.
        /// - Returns: A flat array of all of the results.
    
        func flatMap<T>(request: CNContactFetchRequest, _ transform: (CNContact, UnsafeMutablePointer<ObjCBool>) -> [T]) throws -> [T] {
            var results: [T] = []
    
            try enumerateContacts(with: request) { contact, stop in
                results += transform(contact, stop)
            }
    
            return results
        }
    
        /// Map
        ///
        /// - Parameters:
        ///   - request: The `CNContactFetchRequest`.
        ///   - transform:  A closure that returns a value to be appended to the final results.
        /// - Returns: A flat array of all of the results.
    
        func map<T>(request: CNContactFetchRequest, _ transform: (CNContact, UnsafeMutablePointer<ObjCBool>) -> T) throws -> [T] {
            var results: [T] = []
    
            try enumerateContacts(with: request) { contact, stop in
                results.append(transform(contact, stop))
            }
    
            return results
        }
    }
    

    That’s both a flatMap (for your use-case where you may want to return multiple objects for a given CNContact), as well as a more traditional map rendition (not used here, but is my typical use-case).

    Anyway, you can then use it like so:

    let keys = [
        CNContactPhoneNumbersKey as CNKeyDescriptor,
        CNContactFormatter.descriptorForRequiredKeys(for: .fullName)
    ]
    
    let request = CNContactFetchRequest(keysToFetch: keys)
    request.predicate = NSPredicate(format: "phoneNumbers.@count > 0")
    request.sortOrder = .userDefault
    
    let formatter = CNContactFormatter()
    formatter.style = .fullName
    
    do {
        let results = try store.flatMap(request: request) { contact, _ in
            contact.phoneNumbers.compactMap { phone -> Contact? in
                guard let name = formatter.string(from: contact) else { return nil }
                return Contact(fullName: name, phoneNumber: phone.value.stringValue)
            }
        }
    
        …
    } catch {
        print("Failed to fetch contacts with phone numbers:", error)
    }
    

    That returned:

    Contact(fullName: "John Appleseed", phoneNumber: "888-555-5512")
    Contact(fullName: "John Appleseed", phoneNumber: "888-555-1212")
    Contact(fullName: "Kate Bell", phoneNumber: "(555) 564-8583")
    Contact(fullName: "Kate Bell", phoneNumber: "(415) 555-3695")
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search