Быстрый бросок из замыкания, вложенный в функцию
У меня есть функция, которая выдает ошибку, в этой функции у меня есть 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.