Быстрый бросок из замыкания, вложенный в функцию

У меня есть функция, которая выдает ошибку, в этой функции у меня есть inside a закрытие, что мне нужно, чтобы выбросить ошибку из его обработчика завершения. Это возможно?

Вот мой код до сих пор.

enum CalendarEventError: ErrorType {
    case UnAuthorized
    case AccessDenied
    case Failed
}

func insertEventToDefaultCalendar(event :EKEvent) throws {
    let eventStore = EKEventStore()
    switch EKEventStore.authorizationStatusForEntityType(.Event) {
    case .Authorized:
        do {
            try insertEvent(eventStore, event: event)
        } catch {
            throw CalendarEventError.Failed
        }

    case .Denied:
        throw CalendarEventError.AccessDenied

    case .NotDetermined:
        eventStore.requestAccessToEntityType(EKEntityType.Event, completion: { (granted, error) -> Void in
            if granted {
                //insertEvent(eventStore)
            } else {
                //throw CalendarEventError.AccessDenied
            }
        })
    default:
    }
}

6 ответов

Решение

Когда вы определяете замыкание, которое выбрасывает:

enum MyError: ErrorType {
    case Failed
}

let closure = {
    throw MyError.Failed
}

тогда тип этого замыкания () throws -> () и функция, которая принимает это замыкание в качестве параметра, должна иметь одинаковый тип параметра:

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

Эту функцию вы можете вызвать completion закрытие синхронное:

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

и вы должны добавить throws ключевое слово для подписи функции или завершения вызова с try!:

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

или асинхронный:

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

В последнем случае вы не сможете отловить ошибку.

Так что если completion закрытие в eventStore.requestAccessToEntityType метод и сам метод не имеет throws в своей подписи или если completion называется асинхронно, то вы не можете throw из этого закрытия.

Я предлагаю вам следующую реализацию вашей функции, которая передает ошибку в функцию обратного вызова вместо ее вызова:

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

Поскольку бросок является синхронным, асинхронная функция, которая хочет бросить, должна иметь внутреннее замыкание, которое бросает, например:

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

Затем на сайте вызова вы используете его так:

   insertEventToDefaultCalendar(EKEvent()) { response in
        do {
            try response()
            // Success
        }
        catch {
            // Error
            print(error)
        }
    }

Это невозможно в этом случае - этот обработчик завершения должен быть объявлен с throws (и метод с rethrows) а этот нет.

Обратите внимание, что все эти броски - это просто разные обозначения для NSError ** в Objective-C (параметр ошибки inout). Обратный вызов Objective-C не имеет параметра inout, поэтому нет способа передать ошибку вверх.

Вам придется использовать другой метод для обработки ошибок.

В общем, NSError ** в Obj-C или throws в Swift плохо работают с асинхронными методами, потому что обработка ошибок работает синхронно.

Вы не можете сделать функцию с throw, но вернуть closure со статусом или ошибкой! Если не ясно, я могу дать код.

requestAccessToEntityType делает свою работу асинхронно. Когда обработчик завершения будет запущен, ваша функция уже вернется. Поэтому вы не можете выбросить ошибку из замыкания так, как вы предлагаете.

Вам, вероятно, следует провести рефакторинг кода, чтобы часть авторизации была обработана отдельно от вставки события и только вызова insertEventToDefaultCalendar когда вы знаете, что статус авторизации соответствует ожидаемому / необходимому.

Если вы действительно хотите обрабатывать все в одной функции, вы можете использовать семафор (или аналогичный метод), чтобы асинхронная часть кода работала синхронно с вашей функцией.

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

Простое решение

Несмотря на то, что с помощью большого количества шаблонов можно добиться этого с помощью вложенных замыканий, объем работы, необходимый для достижения чего-то такого простого, очень велик.

Приведенное ниже решение не решает проблему технически, но предлагает более подходящий дизайн для той же проблемы.

Уменьшить

Я считаю, что обработка ошибок лучше фиксируется с помощью переменной экземпляра типа self.error, которые мы обновляем асинхронно и реагируем на них.

Мы могли бы получить реактивные обновления либо через @Published с участием ObservableObject в комбинации или через delegates а также didSetобработчики изначально. Тот же самый принцип реактивной обработки ошибок, независимо от техники, которая, как мне кажется, лучше подходит для этой проблемы и не слишком продумана.

Пример кода

      class NetworkService {

    weak var delegate: NetworkDelegate? // Use your own custom delegate for responding to errors.

    var error: IdentifiableError { // Use your own custom error type.
        didSet {
            delegate?.handleError(error)
        }
    }

    public func reload() {
        URLSession.shared.dataTask(with: "https://newsapi.org/v2/everything?q=tesla&from=2021-07-28&sortBy=publishedAt&apiKey=API_KEY") { data, response, error in
            do {
                if let error = error { throw error }
                let articles = try JSONDecoder().decode([Article].self, from: data ?? Data())
                DispatchQueue.main.async { self.articles = articles }
            } catch {
                DispatchQueue.main.async { self.error = IdentifiableError(underlying: error) }
            }
        }.resume()
    }
}

Примечание

Я пишу до async / await в Swift 5.5, что значительно упрощает эту проблему. Этот ответ по-прежнему будет полезен для резервного копирования <iOS 15, потому что нам нужно использовать GCD.

Другие вопросы по тегам