18

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

Theo Strauss
  • 1,281
  • 3
  • 19
  • 32
  • https://stackoverflow.com/questions/37801370/how-do-i-dispatch-sync-dispatch-async-dispatch-after-etc-in-swift-3 – dan Aug 30 '17 at 18:04
  • Which error are getting ? – GIJOW Aug 30 '17 at 18:08
  • 1
    "am still coming up with errors" Is not a question. Say clearly what the "errors" are and on what line(s). Also, do you really imagine that no one has ever discussed how to authorize Contacts access in Swift 3 before on Stack Overflow? Try searching before asking. Save bandwidth. – matt Aug 30 '17 at 18:09
  • 1
    @matt I dont think that kind of aggression is needed on stack overflow. i added the error, but just chill out – Theo Strauss Aug 30 '17 at 19:19
  • @GIJOW added above. "Instance member 'async' cannot be used on type 'DispatchQueue'; did you mean to use a value of this type instead?" – Theo Strauss Aug 30 '17 at 19:19
  • I don't agree. There's a right way to ask a question. It turns out that this one has nothing to do with Contacts, requesting access, or anything else. It's just that you're trying to use an instance method as if it were a class method. And that would have been quite obvious if you had just cited that one line and shown the error message, and asked about it, right at the start, instead of wasting time and bandwidth with irrelevancies and making us beg for the information you should have provided to start with. – matt Aug 30 '17 at 21:29
  • @matt not only did i ask this question politely and with plenty of information, it absolutely does have to with Contacts *and* requesting access. If you look at the question, you'll see I tried to adapt a Swift 2 function to Swift 3 and there was an error. You could have just answered the question with an answer of "oh this is how you solve that problem, now you know how to do that" or not have commented at all. I appreciate your love for this platform and you striving to make it a better place, but i recommend you contribute positively, or not at all. – Theo Strauss Aug 30 '17 at 21:37
  • There is someone with Stack's God syndrome. @TheoStrauss glad you could solve your problem. – GIJOW Aug 31 '17 at 01:53

2 Answers2

41

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)
}
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • thanks so much! would you recommend deleting the entire function i had and replacing it with this? or adding the showFunctionsAlert() on top of it. – Theo Strauss Aug 30 '17 at 21:33
  • I'm not sure if I follow your question. But this does the requesting of access as well as presenting an alert if denied, so I'm not sure what else you'd need. – Rob Aug 30 '17 at 21:38
  • By the way, I tweaked my answer to take closure parameter, like your original example. My original example was requesting authorization when the view appeared, but it's better to defer this question until the point that the user really needs access (it might be when the view appears, but more likely, it's when they tap on something that obviously requires contacts; if you defer it to the latest possible moment, the user is more likely engaged with your app and more likely to grant permission. – Rob Aug 30 '17 at 21:38
  • hey, sorry commented before i saw your edit. i was asking should i include the function i had in my initial question *and* the two functions above. or do the functions you provided in your answer sufficient to ask the user for permission. thanks so much for all the help – Theo Strauss Aug 30 '17 at 21:40
  • i see what you were aiming to do now. im just about ready to click the checkmark, but one more question. should i call those two functions in the viewdidload or is that not needed. thanks a ton – Theo Strauss Aug 30 '17 at 21:43
  • First, `viewDidLoad` is not a good place to do something that could present some UI. Perhaps `viewDidAppear` is better. But, again, only do that if the view being presented requires contacts. But if the request for contacts is needed later (e.g. when they tap on a button to do something with contacts), then don't put this in `viewDidAppear`, but rather in the `@IBAction` for that button (or what have you). – Rob Aug 30 '17 at 21:46
  • Phenomenal answer, the second part of your answer saved me a ton of time. – MachTurtle May 10 '18 at 17:22
  • How would you go about calling this when the user selects the button? – Christian W Mar 11 '22 at 23:24
  • Er, just call `requestAccess` in your `@IBAction` method. – Rob Mar 11 '22 at 23:26
2

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)
    }
}
Anton Tropashko
  • 5,486
  • 5
  • 41
  • 66