Networking layer

Networking layer

Introducción

No me pregunten la razón por la que me gusta usar playgrounds para hacer, en ocasiones, ejemplos ligeramente complejos. De hecho creo que sin ningún problema se podría replicar el ejemplo de VIPER de este repositorio pero bueno.

Uno de los temas con el que l@s chic@s que van iniciando más se atoran es con Networking, solicitar recursos de internet pues; recuerdo que una vez (y seguramente yo también lo hice en mis primeros acercamientos a esto) vi que alguien utilizó el clásico Type(contentsOf:) en donde se le manda una URL 🤭 y digo, no estoy diciendo que esté mal pero no es lo mejor, ¿ok? A la mejor si sólo tienes una petición de un recurso de internet es completamente válido pero, dime, hoy en día qué aplicación no usa cosas de internet, de hecho no me imagino utilizando muchas apps en modo avión y que funcionen al 100% 🤔.

Pero bueno, basta de chachara, a lo que vinimos; trabajaré un poco con un playground en donde muestro una imagen de Swift completamente centrada y lo iremos modificando para ver como funciona eso del Networking. Entonces el primer punto del proyecto es esta imagen traída del mismo playground, estática en la carpeta de Resources:

let image: UIImage? = UIImage(named: "swift.jpg")
self.imageView.image = image

Networking sin sesión

Ahora, ¿cómo hacemos que esta imagen provenga del internets'? Bueno, la imagen si tiene una URL https://goo.gl/wV9G4I entonces, buscando un poco podemos darnos cuenta que algunos de los tipos del framework Foundation contienen un método para obtener cosas de internet, en nuestro caso Data(contentsOf:) nos ayudará a obtener la información binaria de nuestra imagen y así poder crear una con dicha información (también puedes revisarlo aquí):

guard let url: URL = URL(string: imageURL), let data: Data = try? Data(contentsOf: url) else {
  fatalError("Cannot get data from the URL")
}
let image: UIImage? = UIImage(data: data)
self.imageView.image = image

Pero esto genera una mala práctica, ¿puedes notarlo? Efectivamente, todo este código pasa en el main thread lo que hace que la aplicación se bloquee hasta que no finalice todas las instrucciones, entonces mientras más información a descargar más tiempo bloqueada nuestra interfaz; la solución es simple: la instrucción que trae la información de internet la ejecutamos en un thread diferente al main thread y después, cuando ya tenemos la imagen, la colocamos en el UIImageView pero ahora en el main thread, ¿por qué? Porque TODO lo que tenga que ver con la interfaz TIENE que hacerse en el main thread.

DispatchQueue.global(qos: .default).async {
  guard let data: Data = try? Data(contentsOf: url) else {
    preconditionFailure("There was a problem getting the data from url")
  }  
  let image: UIImage? = UIImage(data: data)
  DispatchQueue.main.sync { self.imageView.image = image }
}

Networking con sesión

Ahora vamos a algo más interesante, los componentes de una petición son:

  • Una sesión
  • Un request
  • Una tarea para procesar la información

Para la sesión podemos usar una nueva o la del sistema AKA el singleton que nos provee URLSession; acá realmente no hay una recomendación, yo por lo regular uso una sesión nueva por temas de inyección de dependencias y así, pero igual puedes usar la shared session si no tienes tanta complejidad en tu capa de networking.

El siguiente paso es crear un URLRequest con nuestra URL del recurso que queremos obtener y acá es donde vamos a colocar los headers y cosas de la configuración de nuestro request pero este ejemplo es sencillo y no requiere tanta configuración.

Por último viene la “tarea” en donde se va a procesar nuestro request en la sesión, hay de varios tipos pero el más común es aquella que nos entrega el Data crudo para nosotros transformarlo a nuestro antojo. La petición se realiza de forma automática en un hilo secundario y el closure de respuesta se ejecuta en el thread en el cual fue hecha la petición, este closure se ejecuta aunque la petición sea incorrecta o tenga algún error, de hecho vienen tres parámetros: Data?, URLResponse, Error?.

let session: URLSession = URLSession(configuration: .default)
let request: URLRequest = URLRequest(url: url)
let dataTask: URLSessionDataTask = session.dataTask(with: request) { data, response, error in
    if let data = data {
        let image: UIImage? = UIImage(data: data)
        DispatchQueue.main.sync { self.imageView.image = image }
    }
}
dataTask.resume()

Refactorización a una clase externa

Ahora pensemos en que pasaría si necesitamos colocar esta imagen en otro lugar de nuestra aplicación, prácticamente tendríamos que duplicar el código; creo que esto no sería una buena solución o, ¿si queremos pedir otra imagen?, ¿qué es lo que cambia?, sólo la URL pero el resto del código sigue siendo el mismo, ¿correcto?, ¿cómo logramos esto?, cuando la vista en la que se va a colocar la imagen no necesariamente es la misma. Un error común en esto es mandar como parámetro un UIImageView pero... ¡NO LO HAGAS! Ya que esto implicaría el import de UIKit sólo para poder usar el tipo UIImageView en el cliente de networking y no, no es un componente de interfaz, ¿entonces cómo resolvemos esto? Bueno, es sencillo, buscaremos un tipo intermedio, por ejemplo CGImage o CIImage y después lo convertiremos a un UIImage, ya en la capa de vista.

final class Network {  
  func getImage(from imageURL: String, _ completion: @escaping (CIImage?) -> ()) {
    guard let url: URL = URL(string: imageURL) else {
      preconditionFailure("Thre was a problem getting the url from string")
    }

    let request: URLRequest = URLRequest(url: url)
    let dataTask: URLSessionDataTask = URLSession.shared.dataTask(with: request) { data, response, error in
      if error != nil {
        preconditionFailure("There was a problem getting the data from url: \(error!.localizedDescription)")
      }

      guard let data = data else {
        preconditionFailure("It was not possible to get data")
      }
      let image: CIImage? = CIImage(data: data)
      completion(image)
    }

    dataTask.resume()
  }
}

Ya después usar esto es super sencillo, ¿o no?

let networker: Network = Network()
networker.getImage(from: "valid_url_to_image") { ciImage in
...
}

Puedes ver la solución completa de esta separación aquí, espero que mi explicación haya sido sencilla y que que muchas de tus dudas hayan sido esclarecidas en vez de quedar pior' que cuando iniciaste a leer este post.

What's next?

Me gusta esta sección, jajajajajoidsjgioearjigoe, ya que se puede interpretar como un reto que te dejo ávid@ lector@, ¿cómo podríamos colocar los protocolos en todo esto y en qué nos ayudarían 🤔?

🚀🌑