iosswiftpromisecncontactcncontactstore

Error "A promise was finished incorrectly" from CNContactStore requestAccess


I am building a UI to request access to CNContactStore and handle the case where a user has previously (and probably erroneously) denied access. When I detect that the current status is .denied, I present a UIAlertController that explains and offers to take them to the app Settings to allow access.

When store.requestAccess() is called while the current status is .denied, the app crashes and shows two errors: 1) "A promise was finished with a nil error." and 2) "A promise was finished incorrectly.". The call stacks are shown below.

I'm not adept at interpreting the call stacks but I think the error is coming from inside CNContactStore. It's not clear to me what I can do to prevent this error.

EDIT: I am not using promise chaining in my app at all.

EDIT2: Clarified above exactly where in my code the error occurs.

import UIKit
import Contacts
import ContactsUI

final class ContactsAppHelper {

static let shared = ContactsAppHelper()

var store = CNContactStore()

func checkAccessStatus(_ completionHandler: @escaping (_ accessGranted: Bool) -> Void) {

    let authorizationStatus = CNContactStore.authorizationStatus(for: .contacts)

    switch authorizationStatus {
    case .authorized:
        completionHandler(true)
    case .denied, .notDetermined:
        store.requestAccess(for: .contacts, completionHandler: { (access, accessError) -> Void in
            if access {
                completionHandler(access)
            }
            else {
                print("access denied")
                DispatchQueue.main.sync {
                    self.showSettingsAlert(completionHandler)
                }
            }
        })
    default:
        completionHandler(false)
    }
}

private func showSettingsAlert(_ completionHandler: @escaping (_ accessGranted: Bool) -> Void) {

    let msg = "This app requires access to Contacts to proceed. Would you like to open settings and grant permission?"
    let alert = UIAlertController(title: nil, message: msg, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "Open Settings", style: .default) { action in
        completionHandler(false)
        UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
    })
    alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { action in
        completionHandler(false)
    })
    let vc = UIApplication.getPresentedViewController()
    vc!.present(alert, animated: true)
}
}


extension UIApplication{
class func getPresentedViewController() -> UIViewController? {
    var presentViewController = UIApplication.shared.keyWindow?.rootViewController
    while let pVC = presentViewController?.presentedViewController
    {
        presentViewController = pVC
    }
    return presentViewController
}
}

These are the call stacks that result:

2019-06-14 15:59:12.220116-0700 Sales Networker[805:28798] [Rx] A promise was finished with a nil error.
2019-06-14 15:59:12.226500-0700 Sales Networker[805:28798] [Rx] Call stack: (
    0   ContactsFoundation                  0x000000012242312a -[CNFuture finishWithError:] + 52
    1   Contacts                            0x000000010e0c2a82 __55-[CNDataMapperContactStore requestAccessForEntityType:]_block_invoke_2 + 187
    2   Contacts                            0x000000010e0ffc71 +[CNAuthorization requestAccessForEntityType:completionHandler:] + 77
    3   Contacts                            0x000000010e072c44 -[CNXPCDataMapper requestAccessForEntityType:completionHandler:] + 123
    4   Contacts                            0x000000010e0c299a __55-[CNDataMapperContactStore requestAccessForEntityType:]_block_invoke + 174
    5   libsystem_trace.dylib               0x000000011269cf00 os_activity_apply_f + 66
    6   Contacts                            0x000000010e116d52 -[_CNContactsLogger requestingAccessForContacts:] + 225
    7   Contacts                            0x000000010e0c28a4 -[CNDataMapperContactStore requestAccessForEntityType:] + 177
    8   Contacts                            0x000000010e09b77f -[CNContactStore requestAccessForEntityType:completionHandler:] + 54
    9   Sales Networker                     0x000000010ccaadc7 $s15Sales_Networker17ContactsAppHelperC17checkAccessStatusyyySbcF + 535
    10  Sales Networker                     0x000000010cce9673 $s15Sales_Networker24OnBoardingViewControllerC17rightButtonTappedyySo8UIButtonCF + 451
    11  Sales Networker                     0x000000010cce9ddc $s15Sales_Networker24OnBoardingViewControllerC17rightButtonTappedyySo8UIButtonCFTo + 60
    12  UIKitCore                           0x0000000119c7d204 -[UIApplication sendAction:to:from:forEvent:] + 83
    13  UIKitCore                           0x00000001196d2c19 -[UIControl sendAction:to:forEvent:] + 67
    14  UIKitCore                           0x00000001196d2f36 -[UIControl _sendActionsForEvents:withEvent:] + 450
    15  UIKitCore                           0x00000001196d1eec -[UIControl touchesEnded:withEvent:] + 583
    16  UIKitCore                           0x0000000119cb5eee -[UIWindow _sendTouchesForEvent:] + 2547
    17  UIKitCore                           0x0000000119cb75d2 -[UIWindow sendEvent:] + 4079
    18  UIKitCore                           0x0000000119c95d16 -[UIApplication sendEvent:] + 356
    19  UIKitCore                           0x0000000119d66293 __dispatchPreprocessedEventFromEventQueue + 3232
    20  UIKitCore                           0x0000000119d68bb9 __handleEventQueueInternal + 5911
    21  CoreFoundation                      0x000000010ebd6be1 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    22  CoreFoundation                      0x000000010ebd6463 __CFRunLoopDoSources0 + 243
    23  CoreFoundation                      0x000000010ebd0b1f __CFRunLoopRun + 1231
    24  CoreFoundation                      0x000000010ebd0302 CFRunLoopRunSpecific + 626
    25  GraphicsServices                    0x0000000114a702fe GSEventRunModal + 65
    26  UIKitCore                           0x0000000119c7bba2 UIApplicationMain + 140
    27  Sales Networker                     0x000000010cca7f6b main + 75
    28  libdyld.dylib                       0x0000000112416541 start + 1
)
2019-06-14 15:59:12.236494-0700 Sales Networker[805:28798] [Rx] A promise was finished incorrectly.
2019-06-14 15:59:12.236582-0700 Sales Networker[805:28798] [Rx] Result: (null)
2019-06-14 15:59:12.236669-0700 Sales Networker[805:28798] [Rx] Error : (null)
2019-06-14 15:59:12.238150-0700 Sales Networker[805:28798] [Rx] Call stack: (
    0   ContactsFoundation                  0x0000000122422ed8 -[CNFuture finishWithResult:error:] + 290
    1   Contacts                            0x000000010e0c2a82 __55-[CNDataMapperContactStore requestAccessForEntityType:]_block_invoke_2 + 187
    2   Contacts                            0x000000010e0ffc71 +[CNAuthorization requestAccessForEntityType:completionHandler:] + 77
    3   Contacts                            0x000000010e072c44 -[CNXPCDataMapper requestAccessForEntityType:completionHandler:] + 123
    4   Contacts                            0x000000010e0c299a __55-[CNDataMapperContactStore requestAccessForEntityType:]_block_invoke + 174
    5   libsystem_trace.dylib               0x000000011269cf00 os_activity_apply_f + 66
    6   Contacts                            0x000000010e116d52 -[_CNContactsLogger requestingAccessForContacts:] + 225
    7   Contacts                            0x000000010e0c28a4 -[CNDataMapperContactStore requestAccessForEntityType:] + 177
    8   Contacts                            0x000000010e09b77f -[CNContactStore requestAccessForEntityType:completionHandler:] + 54
    9   Sales Networker                     0x000000010ccaadc7 $s15Sales_Networker17ContactsAppHelperC17checkAccessStatusyyySbcF + 535
    10  Sales Networker                     0x000000010cce9673 $s15Sales_Networker24OnBoardingViewControllerC17rightButtonTappedyySo8UIButtonCF + 451
    11  Sales Networker                     0x000000010cce9ddc $s15Sales_Networker24OnBoardingViewControllerC17rightButtonTappedyySo8UIButtonCFTo + 60
    12  UIKitCore                           0x0000000119c7d204 -[UIApplication sendAction:to:from:forEvent:] + 83
    13  UIKitCore                           0x00000001196d2c19 -[UIControl sendAction:to:forEvent:] + 67
    14  UIKitCore                           0x00000001196d2f36 -[UIControl _sendActionsForEvents:withEvent:] + 450
    15  UIKitCore                           0x00000001196d1eec -[UIControl touchesEnded:withEvent:] + 583
    16  UIKitCore                           0x0000000119cb5eee -[UIWindow _sendTouchesForEvent:] + 2547
    17  UIKitCore                           0x0000000119cb75d2 -[UIWindow sendEvent:] + 4079
    18  UIKitCore                           0x0000000119c95d16 -[UIApplication sendEvent:] + 356
    19  UIKitCore                           0x0000000119d66293 __dispatchPreprocessedEventFromEventQueue + 3232
    20  UIKitCore                           0x0000000119d68bb9 __handleEventQueueInternal + 5911
    21  CoreFoundation                      0x000000010ebd6be1 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    22  CoreFoundation                      0x000000010ebd6463 __CFRunLoopDoSources0 + 243
    23  CoreFoundation                      0x000000010ebd0b1f __CFRunLoopRun + 1231
    24  CoreFoundation                      0x000000010ebd0302 CFRunLoopRunSpecific + 626
    25  GraphicsServices                    0x0000000114a702fe GSEventRunModal + 65
    26  UIKitCore                           0x0000000119c7bba2 UIApplicationMain + 140
    27  Sales Networker                     0x000000010cca7f6b main + 75
    28  libdyld.dylib                       0x0000000112416541 start + 1
)

Solution

  • I tested your code and found that there are two things happening here.

    The first is that when you call requestAccess(for:completion:) when the status is already .denied, you get a non-fatal stack trace on the console. You can either ignore this or only request access in the case of a .notDetermined status.

    The second problem is related to the synchronous dispatch on the main queue. That causes an access violation for some reason. The solution is to use an asynchronous dispatch. There is no good reason to block the calling queue anyway.

    func checkAccessStatus(_ completionHandler: @escaping (_ accessGranted: Bool) -> Void) {
    
        let authorizationStatus = CNContactStore.authorizationStatus(for: .contacts)
    
        switch authorizationStatus {
        case .authorized:
            completionHandler(true)
        case .denied:
            self.showSettingsAlert(completionHandler)
        case .notDetermined:
            store.requestAccess(for: .contacts, completionHandler: { (access, accessError) -> Void in
                if access {
                    completionHandler(access)
                }
                else {
                    print("access denied")
                    self.showSettingsAlert(completionHandler)
                }
            })
        default:
            completionHandler(false)
        }
    }
    
    
    private func showSettingsAlert(_ completionHandler: @escaping (_ accessGranted: Bool) -> Void) {
    
        let msg = "This app requires access to Contacts to proceed. Would you like to open settings and grant permission?"
        let alert = UIAlertController(title: nil, message: msg, preferredStyle: .alert)
        alert.addAction(UIAlertAction(title: "Open Settings", style: .default) { action in
            completionHandler(false)
            UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
        })
        alert.addAction(UIAlertAction(title: "Cancel", style: .cancel) { action in
            completionHandler(false)
        })
        DispatchQueue.main.async {
            if let vc = UIApplication.getPresentedViewController() {
                vc.present(alert, animated: true)
            } else {
                 completionHandler(false)
            }
        }
    }