Theo Strauss
Theo Strauss

Reputation: 1351

Request Access To Contacts in Swift 3

I want to access the user's Contacts and am planning to do so using the Contacts and ContactsUI framework that Apple supplies.

First, though, I need to ask permission to access the user's contacts and am having trouble doing so. In Swift 2, one could ask permission like so:

func requestForAccess(completionHandler: (accessGranted: Bool) -> Void) {
    let authorizationStatus = CNContactStore.authorizationStatusForEntityType(CNEntityType.Contacts)

    switch authorizationStatus {
    case .Authorized:
        completionHandler(accessGranted: true)

    case .Denied, .NotDetermined:
        self.contactStore.requestAccessForEntityType(CNEntityType.Contacts, completionHandler: { (access, accessError) -> Void in
            if access {
                completionHandler(accessGranted: access)
            }
            else {
                if authorizationStatus == CNAuthorizationStatus.Denied {
                    dispatch_async(dispatch_get_main_queue(), { () -> Void in 
                        let message = "\(accessError!.localizedDescription)\n\nPlease allow the app to access your contacts through the Settings."
                        self.showMessage(message)
                    })
                }
            }
        })

    default:
        completionHandler(accessGranted: false)
    }
}

I tried to convert it to Swift 3 like so, but am still coming up with errors. The error is "Instance member 'async' cannot be used on type 'DispatchQueue'; did you mean to use a value of this type instead?":

func requestForAccess(completionHandler: @escaping (_ accessGranted: Bool) -> Void) {
    let authorizationStatus = CNContactStore.authorizationStatus(for: CNEntityType.contacts)

    switch authorizationStatus {
    case .authorized:
        completionHandler(true)

    case .denied, .notDetermined:
        self.contactStore.requestAccess(for: CNEntityType.contacts, completionHandler: { (access, accessError) -> Void in
            if access {
                completionHandler(access)
            }
            else {
                if authorizationStatus == CNAuthorizationStatus.denied {
                    DispatchQueue.async(group: DispatchQueue.main, execute: { () -> Void in //error here
                        let message = "\(accessError!.localizedDescription)\n\nPlease allow the app to access your contacts through the Settings."
                        self.showMessage(message)
                    })
                }
            }
        })

    default:
        completionHandler(false)
    }
}

Can anyone help out to try to fix this? Any help would be immensely appreciated. Thanks a ton in advance.

Cheers, Theo

Upvotes: 20

Views: 20433

Answers (2)

Anton Tropashko
Anton Tropashko

Reputation: 5816

Same thing, really but it treats no as a no, as opposed to maybe, not now or anything like that. Did you see any fuzzy logic in Apple privacy prompts? I did not.

fileprivate func requestForAccess(completionHandler: @escaping (_ accessGranted: Bool) -> Void) {
    let authorizationStatus = CNContactStore.authorizationStatus(for: CNEntityType.contacts)

    switch authorizationStatus {
    case .authorized:
        completionHandler(true)

    case .notDetermined:
        self.contactStore.requestAccess(for: CNEntityType.contacts, completionHandler: { (access, accessError) -> Void in
            if access {
                completionHandler(access)
            }
            else {
                completionHandler(false)
            }
        })

    default:
        completionHandler(false)
    }
}

Upvotes: 2

Rob
Rob

Reputation: 438232

Instead of

DispatchQueue.async(group: DispatchQueue.main, execute: { ... }

do

DispatchQueue.main.async { ... }

By the way, if permission had previously been denied, there's no point in requesting authorization again, because the OS will not present any "grant access" UI to the end user. It only does that if the user had not previously denied access.

If access to Contacts is truly essential for successful operation of the app, you can show them an alert that gives them the option of going to Settings directly from your app:

func requestAccess(completionHandler: @escaping (_ accessGranted: Bool) -> Void) {
    switch CNContactStore.authorizationStatus(for: .contacts) {
    case .authorized:
        completionHandler(true)
    case .denied:
        showSettingsAlert(completionHandler)
    case .restricted, .notDetermined:
        store.requestAccess(for: .contacts) { granted, error in
            if granted {
                completionHandler(true)
            } else {
                DispatchQueue.main.async {
                    self.showSettingsAlert(completionHandler)
                }
            }
        }
    }
}

private func showSettingsAlert(_ completionHandler: @escaping (_ accessGranted: Bool) -> Void) {
    let alert = UIAlertController(title: nil, message: "This app requires access to Contacts to proceed. Go to Settings to grant access.", preferredStyle: .alert)
    if
        let settings = URL(string: UIApplication.openSettingsURLString),
        UIApplication.shared.canOpenURL(settings) { 
            alert.addAction(UIAlertAction(title: "Open Settings", style: .default) { action in
                completionHandler(false)
                UIApplication.shared.open(settings)
            })
    }
    alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { action in
        completionHandler(false)
    })
    present(alert, animated: true)
}

Upvotes: 42

Related Questions