Swift throw from closure nested in a function

前端 未结 5 1389
醉话见心
醉话见心 2020-12-30 21:15

I have a function that throws an error, in this function I have a inside a closure that I need to throw the error from it\'s completion handler. Is that possibl

相关标签:
5条回答
  • 2020-12-30 21:35

    Because throwing is synchronous, an async function that wants to throw must have an inner closure that throws, such as this:

    func insertEventToDefaultCalendar(event :EKEvent, completion: (() throws -> Void) -> Void) {
        let eventStore = EKEventStore()
        switch EKEventStore.authorizationStatusForEntityType(.Event) {
        case .Authorized:
            do {
                try insertEvent(eventStore, event: event)
                completion { /*Success*/ }
            } catch {
                completion { throw CalendarEventError.Failed }
            }
    
            case .Denied:
                completion { throw CalendarEventError.AccessDenied }
    
            case .NotDetermined:
                eventStore.requestAccessToEntityType(EKEntityType.Event, completion: { (granted, error) -> Void in
                    if granted {
                        let _ = try? self.insertEvent(eventStore, event: event)
                        completion { /*Success*/ }
                    } else {
                        completion { throw CalendarEventError.AccessDenied }
                    }
            })
            default:
                break
        }
    }
    

    Then, at the call site, you use it like this:

       insertEventToDefaultCalendar(EKEvent()) { response in
            do {
                try response()
                // Success
            }
            catch {
                // Error
                print(error)
            }
        }
    
    0 讨论(0)
  • 2020-12-30 21:40

    When you define closure that throws:

    enum MyError: ErrorType {
        case Failed
    }
    
    let closure = {
        throw MyError.Failed
    }
    

    then type of this closure is () throws -> () and function that takes this closure as parameter must have the same parameter type:

    func myFunction(completion: () throws -> ()) {
    }
    

    It this function you can call completion closure synchronous:

    func myFunction(completion: () throws -> ()) throws {
        completion() 
    }
    

    and you have to add throws keyword to function signature or call completion with try!:

    func myFunction(completion: () throws -> ()) {
        try! completion() 
    }
    

    or asynchronous:

    func myFunction(completion: () throws -> ()) {
        dispatch_async(dispatch_get_main_queue(), { try! completion() })
    }
    

    In last case you will not be able to catch error.

    So if completion closure in eventStore.requestAccessToEntityType method and the method itself does not have throws in its signature or if completion is called asynchronously then you can not throw from this closure.

    I suggest you the following implementation of your function that passes error to callback instead of throwing it:

    func insertEventToDefaultCalendar(event: EKEvent, completion: CalendarEventError? -> ()) {
        let eventStore = EKEventStore()
        switch EKEventStore.authorizationStatusForEntityType(.Event) {
        case .Authorized:
            do {
                try insertEvent(eventStore, event: event)
            } catch {
                completion(CalendarEventError.Failed)
            }
    
        case .Denied:
            completion(CalendarEventError.AccessDenied)
    
        case .NotDetermined:
            eventStore.requestAccessToEntityType(EKEntityType.Event, completion: { (granted, error) -> Void in
                if granted {
                    //insertEvent(eventStore)
                } else {
                    completion(CalendarEventError.AccessDenied)
                }
            })
        default:
        }
    }
    
    0 讨论(0)
  • 2020-12-30 21:40

    That's not possible in this case - that completion handler would have to be declared with throws (and the method with rethrows) and this one is not.

    Note that all that throwing is just a different notations for NSError ** in Objective-C (inout error parameter). The Objective-C callback doesn't have an inout parameter so there is no way to pass the error up.

    You will have to use a different method to handle errors.

    In general, NSError ** in Obj-C or throws in Swift don't play well with asynchronous methods because the error handling works synchronously.

    0 讨论(0)
  • 2020-12-30 21:45

    requestAccessToEntityType does its work asynchronously. When the completion handler is eventually run your function already returned. Therefore it is not possible to throw an error from the closure the way you are suggesting.

    You should probably refactor the code so that the authorization part is handled separately from the event insertion and only call insertEventToDefaultCalendar when you know the authorization status is as expected/required.

    If you really want to handle everyhing in one function, you could use a semaphore (or a similar technique) so that the asynchronous code part behaves synchronously with regard to your function.

    func insertEventToDefaultCalendar(event :EKEvent) throws {
        var accessGranted: Bool = false
    
        let eventStore = EKEventStore()
        switch EKEventStore.authorizationStatusForEntityType(.Event) {
        case .Authorized:
            accessGranted = true
    
        case .Denied, .Restricted:
            accessGranted = false
    
        case .NotDetermined:
            let semaphore = dispatch_semaphore_create(0)
            eventStore.requestAccessToEntityType(EKEntityType.Event, completion: { (granted, error) -> Void in
                accessGranted = granted
                dispatch_semaphore_signal(semaphore)
            })
            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
        }
    
        if accessGranted {
            do {
                try insertEvent(eventStore, event: event)
            } catch {
                throw CalendarEventError.Failed
            }
        }
        else {
            throw CalendarEventError.AccessDenied
        }
    }
    
    0 讨论(0)
  • 2020-12-30 21:57

    You can not make function with throw, but return a closure with status or error! If it's not clear I can give some code.

    0 讨论(0)
提交回复
热议问题