Delegation Pattern y Retain Cycles en Swift
El patrón delegation es un patrón que nos permite delegar de una clase A a una clase B para realizar una tarea. Al acabar en nuestra clase B volvemos al flujo de nuestra app (clase A). También veremos lo fácil que es crear retain cycles en nuestro código y por lo tanto generar memory leaks.
Tabla de contenido
Hoy en SwiftBeta vamos a ver el patrón Delegación, este patrón es muy usado en aplicaciones iOS y vas a ver por qué lo solemos utilizar, por ejemplo lo hemos ido viendo durante el curso de UIKit, cuando usábamos UITableViewDelegate, UITableViewDataSource, UICollectionViewDelegate, UICollectionViewDataSource, etc. Este patrón puede implementarse tanto en UIKit y SwiftUI. No depende del framework de UI que estemos utilizando, y incluso en otros lenguajes, es un patrón que cualquier lenguaje orientado a objetos puede utilizar (esto lo comento para que quede muy claro), se puede utilizar con otros lenguajes de programación. Y el delegation pattern quería explicarlo antes de llegar a los próximos videos donde hablaremos de arquitecturas, así será mucho más simple entender por ejemplo, la arquitectura Model-View-Presenter.
Pero ¿qué hace este Patrón? Este patrón sirve por ejemplo, cuando estamos en una clase A y queremos realizar alguna acción que está en otra clase, por ejemplo en una clase B. Cuando la clase B finaliza de hacer la operación que le ha solicitado la clase A, utilizamos este patrón para volver a la clase inicial y seguir con el flujo de nuestra lógica. Cuando volvemos a la clase inicial, en nuestro caso la clase A, puede recibir información que le ha pasado la clase B, de esta manera la clase A puede manipular esos datos.
Vamos a ver un caso real, pero si quieres que siga creando este tipo de contenido, completamente gratis, puedes suscribirte en el canal, de esta manera seguiré creando contenido. Ahora sí, seguimos con el ejemplo, imagina que has cargado la vista de tu ViewController, y al pulsar un UIButton, quieres hacer una petición HTTP para obtener un modelo JSON que necesitas para dibujar en la vista, y justo tienes una clase que se llama APIClient que tiene esta responsabilidad (hemos creado esta clase, para no añadir código al ViewController relacionado con peticiones HTTP, ya que no es su responsabilidad). Es decir, desde nuestra clase A (que es nuestro ViewController) llamamos a nuestra clase B (que es nuestro APIClient).
Cuando el APIClient finaliza de obtener la información, se la pasa al ViewController y muestra la información en la vista. Para hacerlo vamos a utilizar el Delegation Pattern con este mismo ejemplo, cuando pulsemos un UIButton vamos a realizar una petición HTTP para obtener un listado de pokemons, y vamos a mostrar el nombre de uno de ellos en un UILabel. Vas a ver que es muy sencillo.
Creamos el proyecto en Xcode
Lo primero de todo que vamos hacer es crear el proyecto en Xcode. Acuérdate que estamos usando UIKit, es por eso que debes seleccionar Storyboard en Interface.
Creamos la vista del ViewController
Una vez hemos creado el proyecto, nos vamos al ViewController y allí vamos a crear y añadir a la vista un UILabel y un UIButton:
Como hemos dicho, nuestro ViewController va a ser la clase A, la clase que va a solicitar una tarea a otra clase B. Y la llamaremos cuando se pulse el UIButton de la clase A, de nuestro ViewController. Nuestro APIClient va a ser muy sencillo.
Creamos nuestro APIClient
Dentro de nuestra clase, creamos el siguiente método:
Como es normal, el compilador se queja, ya que no hemos creado PokemonResponseDataModel, así que ahora vamos a crear dos structs necesarías para parsear el JSON a un objeto de nuestro dominio:
Perfecto, ya tenemos nuestra clase A (nuestro ViewController) y nuestra clase B (nuestro APIClient). Ahora lo que vamos hacer es crear una instancia de APIClient en nuestro ViewController, de esta manera cuando se pulse el UIButton podemos llamar al método getPokemons().
Conectamos APIClient con ViewController
Para hacerlo creamos una propiedad de apiClient en ViewController, y llamamos al método getPokemons al pulsar el UIButton:
Si compilamos la app y damos al UIButton, vemos como se realiza la petición HTTP correctamente. Pero hemos hecho el camino de IDA y no de VUELTA, queremos que la información obtenida al realizar la petición, se le pase al ViewController para poder mostrar en el UILabel el nombre de un pokemon de forma aleatoria. Ahora es el momento de utilizar el Delegation Pattern, nos vamos a nuestro APIClient, y allí vamos a crear un protocolo:
Este protocolo significa muchas cosas que vamos hacer a continuación:
- Vamos a crear una propiedad llamada delegate en el APIClient que será de tipo APIClientDelegate
- Cuando obtengamos el resultado de la petición HTTP, llamaremos a la propiedad y el método update(pokemons:) pasándole nuestro modelo de dominio.
- En ViewController conformaremos este protocolo, así de esta manera obtendremos el resultado obtenido de nuestro APIClient. Y asignaremos un nombre de un pokemon random al UILabel.
- Para que todo esto funcione, al crear la instancia de APIClient, debemos asignarle la propiedad delegate. Y en esta propiedad delegate le vamos a asignar la instancia de nuestro ViewController.
Nuestra clase APIClient quedaría de la siguiente manera:
Y nuestro ViewController quedaría de la siguiente manera:
Vamos a compilar y vamos a ver qué ocurre. Ahora cada vez que pulsemos nuestro UIButton se hace una petición HTTP y el APIClient nos retorna la lista de pokemons. Nosotros cogemos 1 de forma random y mostramos el nombre en el UILabel de nuestra vista del ViewController.
Retain Cycles
Al utilizar este patrón en iOS hay que prestar atención a no crear un retain cycle. ¿Qué es un retain cycle? Nuestra app tiene una memoria limitada, y hay casos en que si no manejamos bien nuestras instancias de clases en nuestro código, podemos hacer que nuestra app no pare de consumir memoria y por lo tanto nunca sea liberada, acabando en un crash. Un retain cycle ocurre cuando dos instancias tienen referencias fuertes entre ellas.
En nuestro código tenemos un retain cycle que impide que se libere memoria de nuestra app, vamos a ver qué ocurre y cómo lo podemos arreglar.
Nuestra app tiene dos referencias fuertes, de ViewController a APIClient y de APIClient a ViewController. Vamos a copiar y pegar el código de ViewController en un nuevo ViewController2 y vamos a presentarlo desde ViewController, de esta manera verás que cada vez que lo presentamos y dismisseamos los recursos (instancias) que hemos utilizado no desaparecen de la memoria de nuestra app.
Creamos ViewController2
Dentro de este nuevo ViewController vamos a añadir un método que se llama deinit justo al inicio de nuestra clase:
Este método se llamará cuando se libere nuestro ViewController2 de la memoria de nuestra app (también creamos el deinit en nuestro APIClient), y acabará printando un mensaje por consola.
También aprovechamos y cambiamos el backgroundColor de nuestro ViewController2:
Ahora volvemos a nuestro ViewController y vamos a añadir un UIButton para cada vez que se pulse se navegue al ViewController2. Y así es como quedaría nuestro ViewController:
Si compilamos nuestra app, vamos a presentar y dismissear 5 veces nuestro ViewController2:
Una vez lo hemos hecho, ¿cómo podemos saber si hay retain cycles? Vamos a pulsar el siguiente button de Xcode
Al hacerlo, nos aparecerá el grafo de memoria de nuestra app, y podremos ver si tenemos problemas.
Y en nuestro caso, podemos ver que tenemos 5 instancias de nuestro ViewController aún "vivas" en la memoria de nuestra app. Esto es debido a que tenemos 1 o más retain cycles, y que hemos introducido nosotros al programar nuestra app.
Debuggando el grafo de memoria de nuestra app veo que es posible que tengamos dos retain cycles. Vamos a por el más evidente. Al tener una referencia fuerte de ViewController al APIClient y otra referencia fuerte de APIClient a ViewController creamos un retain cycle y nunca se liberaran estas dos instancias. Lo que vamos hacer es crear un referencia débil, y para hacerlo nos vamos a nuestro APIClient y en la property delegate vamos a añadir weak:
Ahora vamos a compilar y vamos a ver si hemos arreglado el retain cycle de nuestra app. Para ello volvemos hacer la misma prueba, de presentar y dimissear ViewController2. Y a medida que lo hacemos vemos por consola:
Esto significa que al romper las strong references que habían, ahora los recursos se liberan de memoria. Quería mostrarte que es muy importante utilizando el Delegation Pattern que no crees referencias fuertes entre instancias de clases y por lo tanto crees retain cycles.
Conclusión
Hoy hemos aprendido a utilizar el delegation pattern con un ejemplo práctico. Dando especial atención a que no creemos retain cycles.