Aprende a usar la Arquitectura Model-View-Controller (MVC) en Swift
La arquitectura Model View Controller es muy usada en aplicaciones iOS. Sobretodo aplicaciones que usan el framework UIKit. Es un arquitectura con 4 componentes bien diferenciados: Modelo, Vista y Controlador.
Tabla de contenido
Hoy en SwiftBeta vamos a empezar una nueva serie sobre arquitecturas. Y en este video vamos a aprender a crear la arquitectura Model View Controller en iOS (e incluso crearemos varios coordinators). Pero ¿qué es una arquitectura? y ¿por qué es importante tenerlo en cuenta al crear una aplicación?
Una arquitectura bien diseñada puede ayudar a garantizar que la aplicación tenga una estructura sólida y bien organizada, lo que puede facilitar su mantenimiento y ampliación en el futuro. Además, una buena arquitectura puede ayudar a asegurar que la aplicación sea fácil de entender y utilizar para otros desarrolladores, este tema es muy importante si trabajas en un equipo con varios iOS developers. También una buena arquitectura puede ayudar a mejorar el rendimiento, la escalabilidad y la testabilidad de la aplicación.
En el video de hoy, vamos a crear una app llamando a la API de Rick and Morty, esto significa que haremos una petición HTTP para mostrar un listado de personajes de la serie, y si pulsamos en una de las celdas, navegaremos una pantalla nueva mostrando el detalle de un personaje. Dentro de esta vista solo aparecerá información del personaje que hemos seleccionado.
Antes de empezar, quiero que imagines la arquitectura de tu app como un motor de coche, con diferentes partes, donde cada parte del motor tiene una función específica. Y juntando todas las piezas haces que funcione.
O también puedes imaginar la arquitectura de tu app como una casa con muchas o pocas habitaciones. Cada habitación sería una capa de tu arquitectura con una responsabilidad.
Puedes tener una casa diáfana y poder ver la cocina, comedor, dormitorio, etc. o con muchas habitaciones, lo importante es tener un orden. Si vas a la cocina y abres la nevera, no quieres encontrarte ropa, o un portátil. Quieres que cada cosa de tu casa esté ordenada y en su sitio. Estos son solo ejemplos para dejarte más claro que una arquitectura te da control y orden.
Al seguir una estructura es más fácil dividir responsabilidades, seguir el código, iterar, etc. y depende de qué aquitectura usemos podemos crear un código más mantenible, escalable y testable. Lo importante es que te sientas cómodo con la arquitectura que estás usando en tu app y que veas que no estás creando clases enormes con mucha responsabilidad.
Esto va por gustos, incluso puedes usar una arquitectura y modificarla para que se adapte mejor a tus necesidades, es decir, podrías seguir la arquitectura MVC y podrías añadir algún componente nuevo que te ayudara a separar más responsabilidades o a testear mejor cierta lógica. Hablando de esto, al final de este video habremos aprendido a usar Model-View-Controller y encima con coordinators (añadiendo una abstracción para desacoplar la navegación dentro de tu app).
Vamos al lío, la primera arquitectura que vamos a ver es Model-View-Controller, pero ¿por qué empezamos por esta? podríamos empezar por cualquier otra, pero vamos a decir que Apple la tuvo presente durante mucho tiempo, sobretodo con el framework UIKit (incluso podemos encontrar información en la documentación para developers)
Model View Controller
¿Qué es la arquitectura MVC? si miras el siguiente diagrama, verás que hay 3 responsabilidades claramente separadas y conectadas
Model, datos que se utilizan para obtener la información que se va a representar en las vistas de nuestra app
View, son una representación visual del modelo que queremos mostrar
Controller, es el mediador, es el que conecta la View con el Model. Cuando recibimos una acción de la View, esta acción la recibe el Controller. Cuando recibe esta acción el Controller controla que lógica ejecutar. Imagina que un User pulsa un UIButton, al pulsarlo, el Controller recibe la acción y ejecuta una la tarea que tenga que realizar, un login, dar un like, dar un follow, etc.
El controlador crea la vista y el modelo y hace de mediador entre estas dos capas. Nunca el modelo se comunica con la vista directamente (y viceversa), todo pasa por el ViewController. Por eso el controlador es la pieza más importante y también es la menos reusable.
La view y el modelo podrías reusarlos en otras partes de tu app (esto lo veremos a continuación).
La arquitectura Model-View-Controller, este patrón de diseño está compuesto por otros patrones (todo esto lo refleja Apple en su documentación y te dejo por aquí el enlace por si quieres echar un vistazo)
Vamos a crear una app muy sencilla, pero nos va a servir para explicar la arquitectura Model-View-Controller.
Creamos el proyecto en Xcode
Al crear el proyecto en Xcode, selecciona como Interface Storyboard, ya que vamos a usar el framework UIKit.
Nada más crear el proyecto, vamos a crear 3 carpetas: Model, View y ViewController. De esta manera iremos organizando nuestro código.
Creamos el APIClient dentro de la carpeta Model
Lo primero de todo que vamos hacer es crear un nuevo fichero en la carpeta Model. Este fichero, esta clase va a ser la encargada de realizar la petición HTTP al backend de rickandmortyapi.com y parsear los datos a un modelo de nuestro dominio. El endpoint que vamos a usar es https://rickandmortyapi.com/api/character y puedes abrir esta URL en tu navegador y ver la respuesta, con todos los campos en su JSON y sus tipos.
Ahora voy a crear el APICLient que va a tener la lógica de realizar peticiones HTTP, y la primera que vamos a implementar es la petición para recibir el listado de personajes:
Ahora vamos a crear dos Structs, estas dos Structs seran modelos de nuestra app que van a servir para transformar el JSON de la petición HTTP a información que entienda nuestra app. Los creamos también en la carpeta Model:
- CharacterModelResponse
- CharacterModel
Nuestra carpeta Model debería contener estos 3 ficheros.
Perfecto, ya tenemos la manera de obtener los modelos. Ahora vamos a crear las Vistas.
Creamos la UITableView y una custom UITableViewCell en la carpeta View
Vamos a crear las Views de nuestra arquitectura. Hemos dicho al inicio que queremos mostrar un listado y en cada celda aparecerá un personaje distinto.
Creamos un nuevo fichero que sea una subclase de UIView. Y lo vamos a llamar CharactersListView. Aquí dentro vamos a crear un UITableView y vamos a añadirla a la vista, con las constraints de Auto Layout por código:
Para poder representar cada personaje dentro del UITableView, necesitamos crear sus celdas. A continuación vamos a crear una nueva Vista y esta vez será una subclase de UITableViewCell, y la vamos a llamar CharacterListCellView
Una vez hemos creado CharacterListCellView, vamos a registrar esta celda en nuestro UITableView de la clase CharactersListView:
De esta manera, el UITableView podrá utilizar más adelante esta celda para mostrar la información que le indiquemos.
Por último, vamos a mover el Storyboard a la carpeta View. Y deberíamos tener la siguiente estructura:
Controllers y Delegates de UITableView
Por último, vamos a centrarnos en el cerebro de esta arquitectura. Vamos a mover el ViewController a la carpeta Controller.
Ahora vamos a crear dos propiedades:
- mainView, que será de tipo CharactersListView. La view que hemos creado hace un momento que contiene el UITableView
- apiClient, necesitamos crear una instancia para realizar la petición HTTP cuando nuestro ViewController se cargue, es decir, cuando su vista aparezca en la pantalla.
A continuación vamos a usar el método loadView para crear la instancia de CharactersListView y vamos a llamar al método que realiza la petición HTTP de nuestro APIClient:
Si ahora compilamos, vemos que se realiza correctamente la petición HTTP y que se parsea el JSON a nuestro modelo. Podemos ver el Array recibido.
Antes de continuar, voy a renombrar este ViewController, lo voy a llamar CharactersListViewController. Para hacerlo, debo renombrarlo en:
- Listado de ficheros
- En el código
- En el inspector de identidad (aparece ViewController en el Storyboard, debemos actualizarlo a CharactersListViewController)
Compilamos para asegurarnos que lo hemos hecho correctamente. Una vez hecho esto, vamos a continuar, ahora vamos a crear los delegates de nuestro UITableView.
Creamos los UITableViewDataSource
Ahora, si has visto el video sobre UITableView, sabrás que necesitamos conformar unos protocolos para el correcto funcionamiento de nuestra UITableView. Vamos a implementar:
- UITableViewDataSource
- UITableViewDelegate
Primero, vamos a crear una clase llamada ListOfCharactersTableViewDataSource y va a conformar el protocolo UITableViewDataSource. Todo esto lo implementamos fuera del ViewController para separar responsabilidades, ¿podríamos añadir este código en el ViewController? Sí, pero mucho mejor si tenemos una clase que encapsule todo este comportamiento.
De momento, fíjate que estamos dando un backgroundColor a la celda. Más tarde volveremos aquí para setear los valores que necesita la celda para mostrar la información del Character
Una vez hemos creado nuestro ListOfCharactersTableViewDataSource vamos a instanciarlo en nuestro ViewController.
- Creamos la propiedad de tipo ListOfCharactersTableViewDataSource
- Creamos una instancia, pasándole la dependencia de nuestro UITableView
- asignamos la instancia que acabamos de crear en 2 al dataSource del UITableView
- Cuando obtenemos el array de Characters se lo pasamos al DataSource del TableView para que los muestre
¿Qué pasa? que ahora se están mostrando todas las celda con el backgroundColor azul. Vamos a arreglarlo. Nos vamos a nuestra celda CharacterListCellView y creamos el siguiente método:
Ahora vamos a nuestro ListOfCharactersTableViewDataSource y vamos a actualizar la línea donde cambiábamos el backgroundColor a .blue:
Si compilamos ahora, podemos ver como se muestran los datos en el UITableView. Pero, queremos mostrar también la imagen de nuestro Character. Para ello, vamos a añadir una dependencia llamada KingFisher, esta dependencia nos permite cargar una image dentro de un UIImageView a partir de una URL.
Añadimos Kingfisher al proyecto
No te preocupes, que es muy fácil añadirla. Tan solo necesitamos la URL del repositorio de Github
Una vez tenemos la ULR nos vamos al Projecto y clickamos en la sección Package Dependencies
Aquí damos al button + y añadimos la URL del repositorio de Github
Una vez ha encontrado el repositorio que queremos añadir, le damos a Add Package y esperamos. Al instalarse veremos en la parte izquierda de nuestro proyecto de Xcode (donde tenemos todo el listado de ficheros) el código añadido de KingFisher.
Volvemos a nuestra celda y vamos hacer dos cosas:
- Importar Kingfisher
2. Añadimos una línea al método que nos configura la celda, y el método quedaría de la siguiente manera:
Vamos a compilar otra vez. Vemos que se muetra la imagen y que ya va cogiendo forma, pero podemos mejorarlo.
Creamos UITableViewDelegate
Ahora vamos a implementar otro delegate de UITableView. Vamos a conformar el UITableViewDelegate en una clase nueva, que vamos a llamar ListOfCharactersTableViewDelegate
Fíjate qué implementación más sencilla, solo vamos a utilizar uno de sus método y es para dar un valor fijo al ancho de las celdas:
Y una vez creado, vamos a crear una propiedad e instanciarlo para asignarselo a nuestro UITableView.
En este caso, vamos a ver paso por paso lo que hemos añadido:
- Propiedad de tipo ListOfCharactersTableViewDelegate
- Creamos una instancia de ListOfCharactersTableViewDelegate
- Asignamos el delegate al UITableView
Ahora, toca compilar para ver el resultado final.
Perfecto! Nuestra carpeta Controller tiene los siguientes ficheros
Y podríamos agrupar la carpeta Model, View y Controller en otra. A la que vamos a llamar ListOfCharacters
Acabamos de estructurar nuestra primera pantalla en Modelos, Views y Controllers. Todo bien organizado, y responsabilidades separadas.
Ahora, como hemos dicho al inicio del video, vamos a crear una pantalla de detalle de un Character. Esto nos va a servir a crear otra estructura de carpetas de Model, View y Controller. También veremos los Coordinators y los problemas que nos solucionan, pero no nos vamos a adelantar. De momento vamos a crear una carpeta llamada CharacterDetail y dentro de esta carpeta vamos a crear una carpeta llamada Model, View y Controller.
Creamos la pantalla de CharacterDetail
Vas a ver lo sencillo que es crear esta pantalla. Lo primero que vamos hacer es crear la nueva vista, y la vamos a llamar CharacterDetailView, y una vez creada, para simplificar nuestro código vamos a copiar el código que hemos usado en CharacterListCellView.
Es la misma vista, pero hemos cambiado 4 partes:
- Subclase de UIView
- Usamos el inicializador acorde a UIView (en lugar de UITableViewCell)
- Añadimos un backgroundColor de white a la vista
- Cambiamos las constraints, pero solo de UIImageView. De esta manera se verá más grande la imagen del Character
Una vez hemos creado la vista en la carpeta correcta
Ahora toca crear el ViewController. Pulsamos COMMAND+N y creamos un ViewController llamado CharacterDetailViewController.
Y va a tener el siguiente código:
Fíjate que en este caso, la carpeta Model está vacía. Esto es debido a que estamos reaprovechado el modelo que ya tenemos en la carpeta Model de la primera pantalla, la de ListOfCharacters.
Lo que vamos hacer ahora, es que cuando pulsemos una celda de nuestro ViewController, se navegue al ViewController CharacterDetailViewController, y para que este se pueda inicializar correctamente, le deberemos pasar un modelo de tipo CharacterModel. Aquí lo estamos haciendo de esta manera, pero quizás para desacoplar este modelo de este ViewController, quizás sería mejor pasarle un ID del Character y que este ViewController llamara al APICLient, con un método nuevo para extraer un Character a partir de un ID. Para simplificar el video vamos a seguir como lo tenemos, le pasamos un simple modelo.
También comentarte, que este modelo es el mismo que el del APIClient. En micaso me gusta separar este modelo en diferentes capas, la capa de Data, la capa de Domain y luego la capa de presentacion. Esto lo veremos en futuros videos, pero una buena práctica es no usar el mismo modelo que nos llega del APIClient, en las capas de presentación.
Detectar tap en una celda del TableView
Vamos a añadir un método en nuestro UITableViewDelegate para detectar cuando un user toca una de las celdas. Necesitamos saber cuando ocurre para realizar la navegación al CharacterDetailViewController.
Vamos a nuestro ListOfCharactersTableViewDelegate y añadimos la siguiente propiedad y método:
Ahora, lo único que tenemos que hacer es ir a nuestro CharactersListViewController , y allí en el viewDidLoad, entre el title y el Task añadimos el siguiente código:
Aquí lo que hacemos es obtener el index de la celda pulsada y obtenemos el modelo a partir de array que tenemos en el dataSource. Una vez obtenemos el modelo, ya podemos presentar programaticamente nuestro CharacterDetailViewController.
Si compilamos, vemos que funciona perfectamente. Pero fíjate que hemos añadido una responsabilidad nueva a nuestro CharactersListViewController, ahora se encarga de saber hacía donde tiene que navegar cuando una celda ha sido pulsada. Lo suyo sería sacar esta responsabilidad a una clase que se encargue de esto. Y es donde entran en juego los coordinators.
Conclusión
Hoy hemos aprendido a cómo usar la arquitectura Model-View-Controller en Swift. La clave de esta arquitectura (y de otras muchas) es entender cómo organizar nuestro código con diferentes responsabilidades. En este caso hay 3 capas bien diferenciadas:
- View
- Model
- Controller
En el siguiente video aprenderemos a cómo añadir un Patrón llamado Coordinator. Este patrón nos permitirá extraer la lógica de navegación que hemos añadido para navegar del CharactersListViewController al CharacterDetailViewController.