Manejo de errores: llamada asincrónica

Estoy creando un marco para los web services utilizados en mi proyecto. He cargado la plantilla en GitHub. https://github.com/vivinjeganathan/ErrorHandling

Tiene varias capas Capa 1 para la validation. Capa 2 para la formación de la request. Capa 3 para la llamada de networking real.

Controlador de vista <—-> Capa 1 <—> Capa 2 <—> Capa 3

Los flujos de datos entre las capas a través de cierres, si el error ocurre en cualquier capa, debe pasar correctamente al ViewController.

Me he referido a este enlace para el event handling errores en llamadas asíncronas – http://appventure.me/2015/06/19/swift-try-catch-asynchronous-closures/ Creado una twig en el mismo repo – nombre – ErrorHandling-Method1 .

Pude transferir el error de la capa 3 a la capa 2 (Nivel único: respuesta de retorno a través de funciones en cierres, como se menciona en el enlace). Pero enfrenta dificultades para volver a transferir a través de varias capas.

¿Alguien puede ayudar con la aplicación de muestra provista en GitHub público?

En primer lugar, no creo que sea necesario astackr las capas como lo hizo, por ejemplo, agregando la funcionalidad de validation como una capa, está aumentando el acoplamiento haciendo que esa capa dependa de las capas a continuación (análisis, creación de networkinges, etc. ), en cambio, ¿por qué no separas la validation para que sea solo dependiente de los datos ?:

class ViewController: UIViewController { var validator = InputValidator() override func viewDidLoad() { super.viewDidLoad() do { try validator.validateInput("INPUT") try Fetcher.requestDataWithParams("INPUT") } catch { handleError(error) } } } 

Ahora la funcionalidad de validation no depende de las otras capas, por lo que la comunicación fluiría de la siguiente manera:

View Controller <—> ParsingLayer <—> NetworkingLayer

Hice cambiar el nombre de las capas, pero no necesariamente tienen que ser así, puedes agregar o quitar capas.

Creo que va a ser algo complicado si trato de explicar mi enfoque, así que voy a dar un ejemplo usando las capas anteriores, primero la capa inferior:

 class NetworkingLayer { class func requestData(params: AnyObject, completion: (getResult: () throw -> AnyObject) -> Void) -> Void { session.dataTaskWithURL(url) { (data, urlResponse, var error) in if let error = error { completion(getResult: { throw error }) } else { completion(getResult: { return data }) } } } } 

He omitido algunas secciones de código, pero la idea es hacer cualquier paso necesario para que la capa funcione (crear session, etc.) y siempre comunicarme de nuevo a través del cierre de finalización; una capa en la parte superior se vería así:

 class ParsingLayer { class func requestObject(params: AnyObject, completion: (getObject: () throw -> CustomObject) -> Void) -> Void { NetworkingLayer.requestData(params, completion: { (getResult) -> Void in do { let data = try getResult() let object = try self.parseData(data) completion(getObject: { return object }) } catch { completion(getObject: { throw error }) } }) } } 

Observe que los cierres de finalización no son los mismos, ya que cada capa agrega funcionalidad, el object devuelto puede cambiar, también observe que el código dentro de la instrucción do puede fallar de dos maneras, primero si falla la llamada de networking y luego si los datos del la capa de networking no se puede analizar; nuevamente, la comunicación con la capa en la parte superior siempre se realiza a través del cierre de finalización.

Finalmente, ViewController puede llamar a la siguiente capa utilizando el cierre esperado por la capa de análisis en este caso, y es capaz de manejar los errores originados en cualquier capa:

 override func viewDidLoad() { super.viewDidLoad() do { try validator.validateInput("INPUT") try ParsingLayer.requestObject("INPUT", completion: { (getObject) in do { let object = try getObject() try self.validator.validateOutput(object) print(object) } catch { self.handleError(error) } }) catch { handleError(error) } } 

Observe que hay una do catch dentro del cierre de finalización, esto es necesario ya que la llamada se realiza de forma asíncrona, ahora que la respuesta ha pasado por todas las capas y realmente ha cambiado para ser de un tipo más especializado, incluso puede validar el resultado sin teniendo la necesidad de crear una capa para la funcionalidad de validation.

Espero eso ayude.

Personalmente, utilizo notifications que pasan el NSError como el object de la notificación en las capas y observo la notificación en el controller de vista.

En las capas:

 NSNotificationCenter.defaultCenter().postNotificationName("ErrorEncountenetworkingNotification", object: error) 

En el controller de vista

 NSNotificationCenter.defaultCenter().addObserver(self, selector: "errorEncountenetworking:", name: "ErrorEncountenetworkingNotification", object: nil) 

el método del selector:

 func errorEncountenetworking(notification: NSNotification!) { let error: NSError! = notification.object as! NSError NSLog("error: \(error)") } 

Identificaste correctamente un problema desagradable con el event handling errores en un código asíncrono.

Parece fácil con funciones síncronas, que simplemente devuelven un código de error, o tienen un parámetro de error adicional, o usan la nueva syntax Swift throws . Aquí hay una function síncrona :

 func computeSome() throws -> Some 

Y esta es una firma de function viable para una function asíncrona :

 func computeSomeAsync(completion: (Some?, NSError?) -> ()) 

La function asíncrona devuelve Void y no arroja. Si falla, llama a su function de finalización con el set de parameters de error.

Sin embargo, los controlleres de finalización se vuelven rápidamente engorrosos, especialmente en el código nested.

La solución es usar un futuro :

 func computeSomeAsync() -> Future<Some> 

Esta function es asíncrona y no arroja, y devuelve un futuro . Entonces, ¿qué es un futuro?

Bueno, un futuro representa el resultado final de una function asíncrona. Cuando llama a la function asíncrona, vuelve inmediatamente y obtiene un marcador de position para el resultado. Esto, llamado futuro , será finalmente completado por la tarea de background subyacente que computa el valor.

Cuando la tarea subyacente finalmente tuvo éxito, este futuro contiene el valor calculado de la function. Cuando falla, contendrá el error.

Según la implementación real y la API de Future Library, puede get el resultado mediante el logging de continuación :

 let future = computeSomeAsync() future.onSuccess { value in print("Value: \(value)") } future.onFailure { error in print("Error: \(error)") } 

Puede parecer extraño al principio, pero puedes hacer cosas increíbles con futuros:

 fetchUser(id).flatMap { user in fetchProfileImage(user.profileImageUrl).flatMap { image in cacheImage(image) } } .onFailure { error in print("Something went wrong: \(error)") } 

La statement anterior es asíncrona, así como la function fetchUser , fetchProfileImage y cacheImage . Manejo de errores incluido.

¿Por qué declarar su método lanza si nunca lanza o incluso intenta atrapar? Podrías lanzar los errores usando la statement lanzable a través de todas las capas, e incluso cambiar el tipo lanzable en cada nivel.

ACTUALIZACIÓN: No pensé en lanzar no funciona en operaciones asíncronas. Usar NSNotification es una buena ruta, o puedes echar un vistazo a RXSwift o similar para resolverlo también. Mi recomendación personal sería utilizar RxSwift. Esto te mantiene fuera del infierno de callback, al que estás viajando actualmente.