Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to resolve an infinite loop in requestAccess(to:completion:) on EKEventStore?

I am switching on EKAuthorizationStatus but even after requestAuthorisation(to:commit:) is called and returned true and no error the switch statement still matches the .notDetermined case and a recursion in it is producing an infinite loop. And it drives me nuts!

I tried to find out how requestAuthorisation(to:commit:) actually works since I have the feeling this problem is all about concurrency or something but I couldn't find anything, so I am having trouble truly reason about the situation.

And since the recursion in my code is definitely part of this infinite loop I tried an approach without recursions. But since the EKAuthorizationStatus could change in between my app's calls to the event store I want to check it beforehand reacting to all it's states accordingly. And so I would have to call my methods for switching over the authorization status and one for requesting it and handle any errors all over my class which I don't want to for reasons of readability, safety and sanity.

private func confirmAuthorization(for entityType: EKEntityType) throws {
    switch EKEventStore.authorizationStatus(for: entityType) {
    case EKAuthorizationStatus.notDetermined:
        // Request authorisation for the entity type.
        requestAuthorisation(for: entityType)
    
        // Switch again.
        try confirmAuthorization(for: entityType)
        
    case EKAuthorizationStatus.denied:
        print("Access to the event store was denied.")
        throw EventHelperError.authorisationDenied
    
    case EKAuthorizationStatus.restricted:
        print("Access to the event store was restricted.")
        throw EventHelperError.authorisationRestricted
        
    case EKAuthorizationStatus.authorized:
        print("Acces to the event store granted.")
    }
}

private func requestAuthorisation(for entityType: EKEntityType) {
    store.requestAccess(to: entityType) { (granted, error)  in
        if (granted) && (error == nil) {
            DispatchQueue.main.async {
                print("User has granted access to \(String(describing: entityType))") // It's being printed over and over
            }
        } else {
            DispatchQueue.main.async {
                print("User has denied access to \(String(describing: entityType))")
            }
        }
    }
}

I expected the switch would match to the .notDetermined case on first launch, where it would request the authorization. So when I switch through the status again it should now match a different case like .authorized or .denied. But actually it matches the .notDetermined case again and the access gets granted over and over. \ >:[

console:

>2019-01-08 12:50:51.314628+0100 EventManager[4452:190572] libMobileGestalt MobileGestalt.c:890: MGIsDeviceOneOfType is not supported on this platform.
>2019-01-08 12:50:54.608391+0100 EventManager[4452:190572] Adding a new event.
>2019-01-08 12:50:54.784684+0100 EventManager[4452:190572] [MC] System group container for systemgroup.com.apple.configurationprofiles path is /Users/***/Library/Developer/CoreSimulator/Devices/********-****-****-****-************/data/Containers/Shared/SystemGroup/systemgroup.com.apple.configurationprofiles
>2019-01-08 12:50:54.785638+0100 EventManager[4452:190572] [MC] Reading from private effective user settings.
>Acces to the event store granted.
>Saved event with identifier: Optional("F8EAC467-9EC2-476C-BF30-45588240A8D0:903EF489-BB52-4A86-917B-DF72494DEA3D")
>2019-01-08 12:51:03.019751+0100 EventManager[4452:190572] Events succsessfully saved.
>User has granted access to EKEntityType
>User has granted access to EKEntityType
>User has granted access to EKEntityType
>[…]
>User has granted access to EKEntityType
>User has granted access to EKEntityType
>User has granted access to EKEntityType
>2019-01-08 12:51:03.291606+0100 EventManager[4452:190572] [Common] _BSMachError: port 26b03; (os/kern) invalid capability (0x14) "Unable to insert COPY_SEND"
>2019-01-08 12:51:03.317800+0100 EventManager[4452:190572] [Common] _BSMachError: port 26b03; (os/kern) invalid capability (0x14) "Unable to insert COPY_SEND"
>User has granted access to EKEntityType
>User has granted access to EKEntityType
>User has granted access to EKEntityType
>[…]
>User has granted access to EKEntityType
>User has granted access to EKEntityType
>User has granted access to EKEntityType
>Acces to the event store granted.
>Preset <EventManager.EventCreationPreset: 0x6000020ca340> needs update.
>Acces to the event store granted.
>Preset <EventManager.EventCreationPreset: 0x6000020ca340> was updated.
>2019-01-08 12:51:03.567071+0100 EventManager[4452:190572] Events succsessfully saved.
>User has granted access to EKEntityType
>User has granted access to EKEntityType
>User has granted access to EKEntityType
>[…]
like image 437
Bernhard Avatar asked Jan 08 '19 15:01

Bernhard


2 Answers

The requestAuthorisation runs asynchronously, so confirmAuthorization Is getting called again before the authorization dialog has even been presented to the user.

Generally, in this sort of pattern (the desire to call something recursively in asynchronous patterns), the solution would be to move the recursive call to into the completion handler of the asynchronous method. But in this case, after the user gets the authorization dialog, they’ll either accept or decline, and there’s no point in worrying about the “what if it’s still not determined” state. So, bottom line, no recursion is needed or desired in this scenario.

That having been said, you obviously do want to get the status back to the caller. But the error throwing pattern won’t work because you need to handle the asynchronous situation (where the permission was not determined and we needed to present a confirmation dialog).

So I’d suggest, instead, that you use completion handler pattern throughout:

private func confirmAuthorization(for entityType: EKEntityType, completion: @escaping (EKAuthorizationStatus) -> Void) {
    let status = EKEventStore.authorizationStatus(for: entityType)

    switch status {
    case .notDetermined:
        requestAuthorisation(for: entityType, completion: completion)

    default:
        completion(status)
    }
}

private func requestAuthorisation(for entityType: EKEntityType, completion: @escaping (EKAuthorizationStatus) -> Void) {
    store.requestAccess(to: entityType) { _, _ in
        DispatchQueue.main.async {
            completion(EKEventStore.authorizationStatus(for: entityType))
        }
    }
}

Or, you can reduce that to a single method:

private func confirmAuthorization(for entityType: EKEntityType, completion: @escaping (EKAuthorizationStatus) -> Void) {
    let status = EKEventStore.authorizationStatus(for: entityType)

    switch status {
    case .notDetermined:
        store.requestAccess(to: entityType) { _, _ in
            DispatchQueue.main.async {
                completion(EKEventStore.authorizationStatus(for: entityType))
            }
        }

    default:
        completion(status)
    }
}

Then you can:

confirmAuthorization(for: .event) { status in
    switch status {
    case .authorized:
        // proceed

    default:
        // handle non-authorized process here
    }
}

// But, remember, the above runs asynchronously, so do *not*
// put any code contingent upon the auth status here. You 
// must put code contingent upon authorization inside the above
// completion handler closure.
//
like image 56
Rob Avatar answered Nov 14 '22 13:11

Rob


// Request authorisation for the entity type.
requestAuthorisation(for: entityType)

is spawning a closure, which is being executed in a background thread. This means that the program continues and the result of this method call will be delivered at some point in the future. Problem is that:

// Switch again.
try confirmAuthorization(for: entityType)

is executed immediately afterwards ~ on the main thread ~ and spawns another background thread. You don't wait for these background threads to finish, before calling another background thread and so on. You need to rework the logic to wait for requestAuthorisation to return something before calling confirmAuthorization again...

like image 2
mistakeNot Avatar answered Nov 14 '22 13:11

mistakeNot