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
)
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)
}
}
}