iosxcodeswiftcontactsaddressbook

How do you access a phone number from your user's contacts in swift?


Here is my code for getting the name of a contact, how would I go about getting their phone number?

func createAddressBook() -> Bool {
    if self.addressBook != nil {
        return true
    }
    var err : Unmanaged<CFError>? = nil
    let addressBook : ABAddressBook? = ABAddressBookCreateWithOptions(nil, &err).takeRetainedValue()
    if addressBook == nil {
        println(err)
        self.addressBook = nil
        return false
    }
    self.addressBook = addressBook
    getContactNames()
    return true
}

func getContactNames() {
    if !self.determineStatus() {
        println("not authorized")
        return
    }
    let people = ABAddressBookCopyArrayOfAllPeople(addressBook).takeRetainedValue() as NSArray as [ABRecord]
    for person in people {
        var contactName = ABRecordCopyCompositeName(person).takeRetainedValue() as String
        self.contacts.append(contact(name: contactName))
    }
}

Any help would be greatly appreciated.


Solution

  • As of iOS 9, we would use Contacts framework, in which phoneNumbers is a CNLabeledValue<CNPhoneNumber>:

    let status = CNContactStore.authorizationStatus(for: .contacts)
    if status == .denied || status == .restricted {
        presentSettingsAlert()
        return
    }
    
    // open it
    
    let store = CNContactStore()
    store.requestAccess(for: .contacts) { granted, error in
        guard granted else {
            self.presentSettingsAlert()
            return
        }
        
        // get the contacts
        
        let request = CNContactFetchRequest(keysToFetch: [CNContactFormatter.descriptorForRequiredKeys(for: .fullName), CNContactPhoneNumbersKey as CNKeyDescriptor])
        do {
            try store.enumerateContacts(with: request) { contact, stop in
                let name = CNContactFormatter.string(from: contact, style: .fullName)
                print(name)
                
                for phone in contact.phoneNumbers {
                    var label = phone.label
                    if label != nil {
                        label = CNLabeledValue<CNPhoneNumber>.localizedString(forLabel: label!)
                    }
                    print("  ", label, phone.value.stringValue)
                }
            }
        } catch {
            print(error)
        }
    }
    

    Where

    private func presentSettingsAlert() {
        let settingsURL = URL(string: UIApplicationOpenSettingsURLString)!
        
        DispatchQueue.main.async {
            let alert = UIAlertController(title: "Permission to Contacts", message: "This app needs access to contacts in order to ...", preferredStyle: .alert)
            alert.addAction(UIAlertAction(title: "Go to Settings", style: .default) { _ in
                UIApplication.shared.openURL(settingsURL)
            })
            alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
            self.present(alert, animated: true)
        }
    }
    

    Prior to iOS 9, you would use the AddressBook framework, in which the phone numbers is a ABMultiValueRef, so get that reference and then iterate through the phone numbers:

    // make sure user hadn't previously denied access
    
    let status = ABAddressBookGetAuthorizationStatus()
    if status == .denied || status == .restricted {
        presentSettingsAlert()
        return
    }
    
    // open it
    
    var error: Unmanaged<CFError>?
    guard let addressBook: ABAddressBook? = ABAddressBookCreateWithOptions(nil, &error)?.takeRetainedValue() else {
        print(String(describing: error?.takeRetainedValue()))
        return
    }
    
    // request permission to use it
    
    ABAddressBookRequestAccessWithCompletion(addressBook) { granted, error in
        if !granted {
            self.presentSettingsAlert()
            return
        }
        
        guard let people = ABAddressBookCopyArrayOfAllPeople(addressBook)?.takeRetainedValue() as [ABRecord]? else {
            print("unable to get contacts")
            return
        }
        
        for person in people {
            let name = ABRecordCopyCompositeName(person)?.takeRetainedValue() as String?
            print(name)
            
            if let phoneNumbers: ABMultiValue = ABRecordCopyValue(person, kABPersonPhoneProperty)?.takeRetainedValue() {
                for index in 0 ..< ABMultiValueGetCount(phoneNumbers) {
                    let number = ABMultiValueCopyValueAtIndex(phoneNumbers, index)?.takeRetainedValue() as? String
                    let label  = ABMultiValueCopyLabelAtIndex(phoneNumbers, index)?.takeRetainedValue()
                    print("  ", self.localizedLabel(label), number)
                }
            }
        }
    }
    

    MacOS has an existing routine to localize that label, but I don't know of any such public function in AddressBook framework for iOS, so you may want to convert it yourself (or populate localization table for NSLocalizedString):

    // frankly, you probably should just use `NSLocalizedString()` and fill the table with these values
    
    private func localizedLabel(_ label: CFString?) -> String? {
        guard let label = label else {
            return nil
        }
        
        if CFStringCompare(label, kABHomeLabel, []) == .compareEqualTo {            // use `[]` for options in Swift 2.0
            return "Home"
        } else if CFStringCompare(label, kABWorkLabel, []) == .compareEqualTo {
            return "Work"
        } else if CFStringCompare(label, kABOtherLabel, []) == .compareEqualTo {
            return "Other"
        } else if CFStringCompare(label, kABPersonPhoneMobileLabel, []) == .compareEqualTo {
            return "Mobile"
        } else if CFStringCompare(label, kABPersonPhoneIPhoneLabel, []) == .compareEqualTo {
            return "iPhone"
        } else if CFStringCompare(label, kABPersonPhoneMainLabel, []) == .compareEqualTo {
            return "Main"
        } else if CFStringCompare(label, kABPersonPhoneHomeFAXLabel, []) == .compareEqualTo {
            return "Home fax"
        } else if CFStringCompare(label, kABPersonPhoneWorkFAXLabel, []) == .compareEqualTo {
            return "Work fax"
        } else if CFStringCompare(label, kABPersonPhoneOtherFAXLabel, []) == .compareEqualTo {
            return "Other fax"
        } else if CFStringCompare(label, kABPersonPhonePagerLabel, []) == .compareEqualTo {
            return "Pager"
        } else {
            return label as String
        }
    }
    

    For Swift 2, see previous revision of this answer.