Arquitectura VIPER en Swift
Arquitectura VIPER en Swift

Introducción a VIPER: Mejora tu Arquitectura en Swift

Curso VIPER en Swift: Un enfoque para mejorar la arquitectura de tus apps. Aprende a implementarlo para un código más limpio y eficiente.

SwiftBeta

Tabla de contenido


👇 SÍGUEME PARA APRENDER SWIFTUI, SWIFT, XCODE, etc 👇
Aprende a usar la Arquitectura VIPER en Swift - PARTE 1
Aprende a usar la Arquitectura VIPER en Swift - PARTE 1

Hoy en SwiftBeta vamos a crear una aplicación con la arquitectura VIPER. La app que vamos a crear va a ser muy sencilla pero completa. Para entender la arquitectura VIPER, lo que haremos será usar una API para mostrar un listado de películas, y al pulsar en una de las películas navegaremos al detalle de esta. Al crear esta aplicación entenderemos cómo funciona VIPER y así podrás crear tus propias aplicaciones siguiendo esta arquitectura. Pero, ¿qué es VIPER? VIPER es una de las arquitecturas que podemos usar al crear una app iOS. Igual que puedes crear una app con la arquitectura Model-View-Controller, Model-View-Presenter, Model-View-ViewModel, etc. VIPER se utiliza para organizar el código de una app en módulos pequeños y reutilizables. Cada módulo tiene una responsabilidad específica, lo que ayuda a mantener el código limpio y fácil de mantener. También nos ayuda a testear mejor las diferentes capas de nuestra app.

El nombre VIPER es un acrónimo de:

  • View
  • Interactor
  • Presenter
  • Entity
  • Router

Quiero que pienses en estos componentes como diferentes piezas de lego, y al unirlas creamos una pantalla de nuestra app. Es decir, al crear una View, Interactor, Presenter, Entitity y Router, creamos una pantalla. Al crear la pantalla de nuestra app podríamos decir que es uno de esos módulos pequeños que te comentaba. Y al crear y conectar varios módulos puedes construir una aplicación con diferente funcionalidad. En nuestra app vamos a crear 2 módulos, uno para listar películas y otro para ver el detalle de la película. Cuando tengamos estos 2 módulos los conectaremos para que al pulsar en una celda de nuestra app, navegue automáticamente al detalle de la película.

Arquitectura VIPER

Ahora, vamos a entrar un poco en detalle en qué significa cada componente de VIPER. Voy a seguir el siguiente orden (añadir dibujo)

Pero antes, si quieres apoyar el contenido que subo cada semana, suscríbete. De esta manera seguiré publicando contenido completamente grautito en Youtube. También comentarte que tengo un libro de Swift con varios capítulos y ejercicios prácticos totalmente en castellano. Al adquirir el libro tendrás todo el código de cada capítulo en Playgrounds y también la solución de cada ejercicio, te dejo el enlace en la descripción del video.
  • View, es la vista que acabamos viendo por pantalla, esta vista mostrará la información necesaria a un user. Por ejemplo, si estamos en un login, mostraremos un formulario con el campo email y password con un button, o si estamos en un listado de películas mostraremos un UITableView o UICollectionView donde el user podrá hacer Scroll y ver todas las películas listadas.
  • Presenter, el siguiente componente es el Presenter. Este componente tiene una doble misión en la arquitectura de VIPER. Por una lado se encarga de recibir acciones de la View, por ejemplo en una View con un formulario de email y password se pulsa el Button de Login, este comando, esta acción se le envía al Presenter para que gestione la tarea que debe realizar. O en nuestro caso, el ejemplo que veremos hoy cuando la View de nuestra app de películas se muestre, tendremos que mostrar las películas, en esta ocasión llamamos al Presenter para que realice la acción que deba hacer. Spoiler llama a otro componente de la arquitectura de VIPER.
    A parte de recibir acciones de la View, por otro lado, el Presenter se encarga de presentar los datos a la View. Es decir, imagina que queremos mostrar un listado de películas. El Presenter recibe esta información de una capa inferior y se la pasa a la vista (a una capa superior) para que la muestre por pantalla. La información que le pasa el Presenter a la View está totalmente formateada, es decir, el presenter realiza la lógica de presentación, de esta manera la View no debe hacer nada, solo mostrar lo que le está pasando (esto implica formatters, modificar textos, etc)
  • Interactor, ahora continuamos con otra pieza fundamental. El Presenter cuando recibe acciones de la View, ¿a quién se las envía? Se las envía al Interactor. El interactor sabe dónde debe ir para recuperar la información que le ha pedido el Presenter, dentro del Interactor tenemos la lógica de negocio. Dentro de esta lógica se puede obtener la información de varias fuentes, dependerá de nuestra aplicación, pero un uso muy común es realizar una petición HTTP, a la base de datos, etc. El Interactor es el encargado de irnos a buscar esta información que nos pide el Presenter.

De momento hemos visto 3 componentes, para resumir siguiendo con los mismos ejemplos de antes, imagina que estás haciendo un Login. En la View el user añade el email y password y selecciona el Button Login para loguearse, esta acción la recibe el Presenter (que es el mediador entre la View y el Interactor) y el Presenter llama al Interactor para que haga la petición HTTP con la información del email y password que ha viajado desde la View. Una vez el Interactor recibe la respuesta de la petición HTTP envía la respuesta al Presenter para que decida qué hacer con esa información. Si mostrar un error en la pantalla (por que los datos introducidos eran incorrectos) o dar acceso al contenido de la app y por lo tanto navegar a una nueva pantalla, una nueva View. Y para realizar esta navegación entra en juego otro componente de VIPER

  • Router, este componente se llama Router. Los routers nos permiten navegar a otras pantallas de nuestra app. Son los encargados de recibir una orden de nuestro Presenter indicando hacía dónde queremos navegar. Y una vez navegamos a una nueva pantalla, a un nuevo módulo, encontramos otra vez los mimos componentes View, Presenter, Interactor, Router que se encargan de que esa pantalla funcione y muestre los datos que tenga que mostrar.
  • Entities, y quizás te estás preguntando, vale, hemos visto la View, Presenter, Interactor y Router, pero nos faltan los Entities. Los Entities son modelos de nuestra lógica de negocio. En este video, son los modelos que vamos a usar al transformar nuestro JSON al realizar la petición HTTP a un modelo de nuestro dominio. Las structs que usaremos para transformar el JSON a nuestro modelo serán las Entities.

Como veremos en el video de hoy, algunos de estos componentes se podrán comunicar con otros componentes a través de protocolos, a través de interfaces. De esta manera haremos que nuestro código esté más desacoplado y podamos trabajar con abstracciones en lugar de implementaciones concretas. Esto tiene muchos beneficios y uno de ellos es que es hace más fácil que podamos testear nuestro código. Una vez hemos hecho un breve repaso de la arquitectura VIPER, vamos a ponerlo en práctica. Vamos a crear 2 pantallas, en la primera mostraremos un listado de películas y al pulsar una de las películas navegaremos al detalle de la película.

Creamos proyecto en Xcode

Lo primero de todo que vamos hacer es crear nuestro proyecto en Xcode. Selecciona en Interface Storyboard ya que vamos a usar el framework UIKit. A continuación vamos a crear un grupo nuevo y vamos a crear 5 ficheros:

  • ListOfMoviesView
  • ListOfMoviesPresenter
  • ListOfMoviesInteractor
  • ListOfMoviesRouter
  • Y creamos otra carpeta llamada Entities

Vamos a empezar por nuestro Interactor, aquí dentro vamos a crear una class que tenga un único método. Este método se va a encargar de realizar una petición HTTP. Aquí podríamos usar Repositorios, Datasource, APIClient, etc pero para simplificar y centrarnos 100% en VIPER vamos a realizar la petición directamente desde el Interactor. Pero ¿dónde realizamos la petición HTTP? Vamos a usar The Movie Database para extraer la información de películas. Para hacerlo necesitamos crearnos una cuenta y en cuestión de segundos tenemos un token que enviaremos en cada petición HTTP.

¿Qué API vamos a usar?

Vamos a verlo paso por paso, lo primero de todo nos creamos una cuenta en el siguiente enlace

The Movie Database (TMDB)
The Movie Database (TMDB) is a popular, user editable database for movies and TV shows.

Una vez tenemos la cuenta creada, nos vamos a la SETTINGS -> API y seleccionamos generar una nueva API KEY para developers. Nos aparecerá un formulario, aquí rellenamos la información. Y al hacerlo ya tenemos el API KEY listo para usar en nuestras peticiones HTTP.

Una vez tenemos el API KEY creado podemos echar un vistazo a la documentación de la API. Aquí encontraremos toda la información necesaria

API Docs
Hosted API documentation for every OAS (Swagger) and RAML spec out there. Powered by Stoplight.io. Document, mock, test, and more, with the StopLight API Designer.

Podemos ver el endpoint que vamos a usar y ver también la respuesta que vamos a recibir. Vamos a usar este endpoint para obtener las películas más populares.

API Docs
Hosted API documentation for every OAS (Swagger) and RAML spec out there. Powered by Stoplight.io. Document, mock, test, and more, with the StopLight API Designer.

Y podemos investigar la respuesta del JSON de esta manera podemos crear nuestro modelo dentro de la app, nuestro primer Entity. En este caso vamos a recibir un Array de movies que viene dentro de la Key results.

Entity

Una vez hemos repasado la API y tenemos toda la información, ya podemos crear nuestro Entity en la carpeta que hemos creado hace un momento. Lo vamos a llamar PopularMovieEntity

struct PopularMovieEntity: Decodable {
    var id: Int
    var title: String
    var overview: String
    var imageURL: String
    var votes: Double
    
    enum CodingKeys: String, CodingKey {
        case id
        case title
        case overview
        case imageURL = "poster_path"
        case votes = "vote_average"
    }
}
Creamos nuestro primer Entity

Y ahora vamos a crear otro Entity llamado PopularMovieResponseEntity. Este Entity, este modelo va a tener todo el Array de movies en su única propiedad.

struct PopularMovieResponseEntity: Decodable {
    let results: [PopularMovieEntity]
}
Creamos el segundo Entity de nuestra app

Interactor

Con estos 2 Entities ya podemos construir el método de nuestro Interactor:

class ListOfMoviewsInteractor {
    func getListOfMovies() async -> PopularMovieResponseEntity {
        let url = URL(string: "https://api.themoviedb.org/3/movie/popular?api_key=02face8651c1fbb596638eaa99e07790")!
        let (data, _) = try! await URLSession.shared.data(from: url)
        return try! JSONDecoder().decode(PopularMovieResponseEntity.self, from: data)
    }
}
Creamos el método en el Interactor para realizar la petición HTTP

Es un método muy sencillo que hemos visto muchas veces en los videos del canal. Pero voy a explicarlo paso por paso:

  1. Creamos la URL, el endpoint al que vamos a llamar para realizar la petición HTTP. Fíjate que hemos añadido un parámetro que es el API Key que hemos creado hace un momento.
  2. Realizamos la petición HTTP y guardamos los datos en la variable data
  3. Transformamos los datos de la petición HTTP a nuestro Entity llamado PopularMovieResponseEntity
  4. Retornamos el Entity

Ya hemos creado 2 componentes de nuestra arquitectura. Hemos creado los Entities y el Interactor. Ahora vamos a por la siguiente capa de nuestra arquitectura VIPER, vamos a crear el Presenter.

Presenter

En el diagrama de la arquitectura de VIPER hemos visto que un Presenter tiene referencias al Interactor, Router y View. Poco a poco iremos creando estas referencias, ahora como siguiente paso vamos a crear la referencia al Interactor.

class ListOfMoviesPresenter {    
    private let listOfMoviesInteractor: ListOfMoviewsInteractor
    
    init(listOfMoviesInteractor: ListOfMoviewsInteractor = ListOfMoviewsInteractor()) {
        self.listOfMoviesInteractor = listOfMoviesInteractor
    }
}
Creamos una referencia del Interactor en el Presenter

Ahora mismo el Interactor tiene una referencia, una propiedad del Interactor pero no la está utilizando. Lo que vamos hacer es crear un método llamado onViewAppear y dentro de este método el Presenter llame al Interactor, al único método que tiene implementado para realizar la petición HTTP.

class ListOfMoviesPresenter {    
    private let listOfMoviesInteractor: ListOfMoviewsInteractor
    
    init(listOfMoviesInteractor: ListOfMoviewsInteractor = ListOfMoviewsInteractor()) {
        self.listOfMoviesInteractor = listOfMoviesInteractor
    }
    
    func onViewAppear() {
    	Task {
        	let models = await listOfMoviesInteractor.getListOfMovies()
        }
    }
}
Creamos un método en el Presenter para llamar al método del Interactor

El método onViewAppear lo llamaremos desde la View en unos minutos. Fíjate que  nuestro Presenter llama al Interactor para que realice una acción y luego el Interactor devuelve el resultado y lo almacena en una constante llamada models. Esta información, este modelo se lo tenemos que pasar a la View para que lo muestre por pantalla. Y ¿cómo lo hacemos? Muy sencillo, vamos a usar el Patrón Delegation.

Creamos un protocolo en nuestro Presenter llamado ListOfMoviesUI con un único método:

protocol ListOfMoviesUI: AnyObject {
    func update(movies: [PopularMovieEntity])
}
Protocolo para comunicarnos con la vista y pasarle los modelos de las películas

Haremos que la View sea el delegado del Presenter, de esta manera cuando reciba los datos el Presenter se los pasará a la View para que los muestre. Para que la View pueda ser el delegate, debemos crear una propiedad en el Presenter de tipo ListOfMoviesUI. Y muy importante tiene que ser WEAK, de esta manera evitamos retain cycles en nuestra app. El presenter tiene referencias fuertes al Interactor y al Router, pero tiene referencia débil a la View.
La propiedad la marcamos como opcional y nos vamos al método onViewAppear. Aquí una vez obtenemos los modelos que queremos mostrar en la View, se los pasamos al Delegate, que en nuestro caso será la View.

func onViewAppear(){
   Task {
     let models = await listOfMoviesInteractor.getListOfMovies()
     ui?.update(movies: models.results)
   }
}
Pasamos a nuestro delegado el listado de películas (nuestras entidades)

Ya tenemos el presenter acabado, luego volveremos a él pero ahora toca crear nuestra View, la parte visual de nuestra aplicación.

View

De momento nuestra View será una subclase de UIViewController llamada ListOfMoviesView. En otros videos hemos visto como separar la View del ViewController, pero en este caso para no complicarnos trataremos el ViewController como la View. Dentro de nuestra View creamos una propiedad llamada presenter de tipo ListOfMoviesPresenter opcional y dentro del método viewDidLoad de nuestro ViewController llamamos al método onViewAppear de nuestro Presenter:

class ListOfMoviesView: UIViewController {
    var presenter: ListOfMoviesPresenter?
    
    init() {
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .blue
        presenter?.onViewAppear()
    }
}
Creamos la View de nuestra arquitectura VIPER

Ahora ya tenemos casi creado el camino de ida y vuelta, es decir, cuando aparece la View se llama al Presenter, que a su vez llama al Interactor para realizar la operación HTTP. Al obtener la respuesta el Interactor retorna el modelo que se almacena en una constante del Presenter y finalmente el Presenter se lo pasa a la View. Este último paso es el que nos falta por crear, el paso donde el Presenter le pasa los datos a la View. Para hacerlo es muy sencillo, tan solo debemos especificar que nuestra View conforme el protocolo ListOfMoviesUI y por lo tanto implementar el método update(movies:):

extension ListOfMoviesView: ListOfMoviesUI {
    func update(movies: [PopularMovieEntity]) {
        print("Datos recibidos \(movies)")
    }
}
Conformamos el protocolo ListOfMoviesUI para recibir el listado de películas

Ahora ya tenemos nuestro camino de ida y vuelta creado. Tenemos todas las piezas de lego creadas. Ahora tenemos que unirlas para que nuestra vista se comporte tal y como queremos. Esta unión la vamos hacer en nuestro Router. Aquí vamos a crear la class ListOfMoviesRouter con un único método llamado showListOfMovies.

Router

Dentro de este método creamos una instancia de nuestra View y Presenter.

class ListOfMoviesRouter {
    func showListOfMovies(window: UIWindow?) {
        let view = ListOfMoviesView()
        let presenter = ListOfMoviesPresenter()
        presenter.ui = view
        view.presenter = presenter
        
        window?.rootViewController = view
        window?.makeKeyAndVisible()
    }
}
Unimos todas nuetras piezas en el Router

Una vez creamos la instancia de la View y Presenter. Ahora debemos crear la unión de estos dos componentes. A la View le asignamos la instancia del Presenter, y al Presenter le asignamos el Delegado. En este caso el delegado es la View, es donde queremos recibir la información que nos retorna el Presenter para poderla mostrar en la View.

Finalmente, asignamos como rootViewController la View y mostramos la window.

Antes de compilar hay que eliminar:

  • Storyboard llamado Main del listado de ficheros
  • Nos vamos al Info.plist y eliminamos el main storyboard y dentro del scene manifest

Una vez hemos hecho estos 3 pasos, ahora vamos al SceneDelegate. Aquí aprovechamos y eliminamos métodos que no vamos a usar en nuestra app y creamos una propiedad de tipo ListOfMoviesRouter, la vamos a llamar listOfMoviesRouter y la vamos a usar para navegar a nuestro listado de movies.
Para hacerlo nos vamos al primer método y añadimos el siguiente código:

var listOfMoviesRouter = ListOfMoviesRouter()   
    
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    guard let windowScene = (scene as? UIWindowScene) else { return }
    
    window = UIWindow(windowScene: windowScene)
            
    listOfMoviesRouter.showListOfMovies(window: window)
}
Llamamos al router que acabamos de crear en el punto de entrada de nuestra app

Si ahora compilamos deberíamos ver una pantalla azul y en consola toda la información recibida al haber realizado correctamente la petición HTTP. La información que se muestra son nuestro modelos creados al inicio del video.

Antes de continuar explorando VIPER, vamos a crear la UI de nuestra app.

View: Celdas de nuestra UITableView

Para crear la View de nuestra app, vamos a crear un UITableView y vamos a asignar una película a cada celda. Es decir, una celda de nuestra UITableView será una movie de nuestro Array. A continuación voy a crear la representación visual de cada celda:

import Foundation
import UIKit

class MovieCellView: UITableViewCell {
    let movieImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.contentMode = .scaleAspectFit
        imageView.translatesAutoresizingMaskIntoConstraints = false
        return imageView
    }()
    
    let movieName: UILabel = {
        let label = UILabel()
        label.numberOfLines = 2
        label.font = .systemFont(ofSize: 32, weight: .bold, width: .condensed)
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    let movieDescription: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0
        label.font = .systemFont(ofSize: 12, weight: .regular, width: .standard)
        label.textColor = .gray
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        setupViews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setupViews() {
        addSubview(movieImageView)
        addSubview(movieName)
        addSubview(movieDescription)
        
        NSLayoutConstraint.activate([
            movieImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
            movieImageView.topAnchor.constraint(equalTo: topAnchor, constant: 12),
            movieImageView.heightAnchor.constraint(equalToConstant: 200),
            movieImageView.widthAnchor.constraint(equalToConstant: 100),
            movieImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
            
            movieName.leadingAnchor.constraint(equalTo: movieImageView.trailingAnchor, constant: 18),
            movieName.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
            movieName.topAnchor.constraint(equalTo: movieImageView.topAnchor, constant: 24),
            
            movieDescription.leadingAnchor.constraint(equalTo: movieImageView.trailingAnchor, constant: 20),
            movieDescription.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
            movieDescription.topAnchor.constraint(equalTo: movieName.bottomAnchor, constant: 8),
        ])
    }
}
Celda que mostrará la información de cada película

Esta celda es muy sencilla, contiene un UIImageView, y 2 Labels. La UIImageView es para mostrar la imagen de la película y los UILabels son para mostrar el título de la película y una pequeña descripción.

Esta celda necesita alimentarse con datos de la película, y esta información se la proporcionará nuestra View llamada ListOfMoviesView. Para poder configurar las subvistas de nuestra celda con el título de la película, descripcion e imagen vamos a  crear un método.

func configure(model: PopularMovieEntity) {
    movieName.text = model.title
    movieDescription.text = model.overview
}
Método para configurar la información de la celda

Este método se llamará cada vez que se configure una celda, pasándole los valores de la película. En este caso también queremos mostrar la portada de nuestra película de forma asícrona, y para hacerlo vamos a usar una dependencia externa llamada KingFisher. Esta dependencia la vamos a añadir con Swift Package Manager, el gestor de dependencias de Xcode.

Dependencia externa: Kingfisher

Para añadir esta dependencia podemos buscar en google kingfisher github y compiamos la URL, una vez tenemos laURL nos vamos a la sección de Swift Package Manager y la añadimos (https://github.com/onevcat/Kingfisher)

GitHub - onevcat/Kingfisher: A lightweight, pure-Swift library for downloading and caching images from the web.
A lightweight, pure-Swift library for downloading and caching images from the web. - GitHub - onevcat/Kingfisher: A lightweight, pure-Swift library for downloading and caching images from the web.

Una vez hemos añadido la dependencia a nuestro proyecto, volvemos a nuestra celda y lo primero que hacemos es importar Kingfisher al inicio de nuestro fichero, a continuación implementamos la siguiente línea para que se muestre la imagen de la película:

func configure(model: PopularMovieEntity) {
    movieImageView.kf.setImage(with: URL(string: "https://image.tmdb.org/t/p/w200" + model.imageURL))
    movieName.text = model.title
    movieDescription.text = model.overview
}
Usamos KingFisher para cargar la imagen de la película

Fíjate que hemos hardcodeado el inicio de una URL y lo hemos concatenado con información de nuestro modelo, esta parte la mejoraremos en los próximos minutos, así la vista no tendrá la responsabilidad de saber que tenemos que concatenar esta URL para poder mostrar la imagen. Con este cambio ya tenemos acabada la celda, ahora vamos a crear la UITableView que contendrá la celda que acabamos de crear.

Dentro de nuestro ListOfMoviesView creamos la UITableView en una propiedad llamada moviesTableView:

class ListOfMoviesView: UIViewController {
    private var moviesTableView: UITableView = {
        let tableView = UITableView()
        tableView.translatesAutoresizingMaskIntoConstraints = false
        tableView.estimatedRowHeight = 120
        tableView.rowHeight = UITableView.automaticDimension
        tableView.register(MovieCellView.self, forCellReuseIdentifier: "MovieCellView")
        return tableView
    }()
    ...
}
Programamos nuestra tableview, donde se mostraran todas las películas

Una vez tenemos la UITableView la añadimos a la jerarquía de vistas, en este caso la vamos a añadir a la view de nuestro ViewController ListOfMoviesView y vamos a aplicar las constraints de Auto Layout:

private func setupTableView() {
    view.addSubview(moviesTableView)
    NSLayoutConstraint.activate([
        moviesTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
        moviesTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
        moviesTableView.topAnchor.constraint(equalTo: view.topAnchor),
        moviesTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
    ])
  
}
Añadimos las constraints de nuestra UITableView

Este método lo llamamos en el viewDidLoad, justo antes de llamar al método onViewAppear del Presenter:

override func viewDidLoad() {
    super.viewDidLoad()
    
    view.backgroundColor = .blue
    
    setupTableView()
    
    Task {
        await presenter?.onViewAppear()
    }
}
Llamamos al método setupTableView en el viewDidLoad

Todo este proceso ya lo hemos visto en el canal varias veces, no es nada nuevo. Ahora la UITableView ya está en la jerarquía de vistas, pero debemos darle información para mostrar, le debemos dar modelos que pueda representar en sus celdas. Nutrir con datos para que podamos hacer scroll dentro de la UITableView. Para hacerlo vamos a implementar el dataSource de nuestra TableView.

Implementamos UITableViewDataSource

Lo primero de todo es añadir esta linea, al final del método setupTableView()

moviesTableView.dataSource = self
El dataSource de la TableView será ListOfMoviesView

Aquí le estamos indicando que la clase donde se van a implementar los métodos del protocolo UITableViewDataSource va a ser en self, es decir en ListOfMoviesView, la clase en la que estamos (en otros videos creábamos una clase aparte para tener esta responsabilidad, pero en este video la implementaremos en la misma clase).

Nos vamos abajo de nuestro código y creamos una extensión en ListOfMoviesView, una vez creada la extensión vamos a indicar que conforme el protocolo UITableViewDataSource. Al hacerlo, debemos implementar estos dos métodos:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    // TODO
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    // TODO
}
Tenemos que conformar los métodos de UITableViewDataSource

Estos método ya los hemos visto en varios videos del canal. El primer método nos va a indicar el número de elementos, el número de celdas que va a tener nuestra UITableView. En nuestro caso hemos dicho que tendremos tantas celdas como películas (estas películas las recibimos al hacer la petición HTTP).

El segundo método se va a encargar de crear una representación visual de nuestro modelo, de nuestra película en una celda.

Para saber cuántos elementos vamos a mostrar en nuestra celda, debemos guardar una referencia del Array de películas cuando realicemos la petición HTTP. Este modelo lo podemos crear en el Presenter.

class ListOfMoviesPresenter {
    weak var ui: ListOfMoviesUI?
    
    private let listOfMoviesInteractor: ListOfMoviewsInteractor
    var models: [PopularMovieEntity] = []
    
    init(listOfMoviesInteractor: ListOfMoviewsInteractor = ListOfMoviewsInteractor()) {
        self.listOfMoviesInteractor = listOfMoviesInteractor
    }
    
    func onViewAppear() async {
        models = await listOfMoviesInteractor.getListOfMovies().results
        ui?.update(movies: models)
    }
}
Guardamos una referencia de los modelos recibidos por el Interactor en el Presenter

Dentro del presenter hemos creado la propiedad models y también hemos asignado a esta propiedad los valores recibidos del Interactor. Ahora podemos volver a nuestra View y hacer lo siguiente:

func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
    presenter!.models.count
}
El número de celdas será el número de elementos del Array models

Luego mejoraremos este código y quitaremos este force unwrap del Presenter, pero antes vamos a acabar de implementar el siguiente método, vamos a ello:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "MovieCellView", for: indexPath) as! MovieCellView
    cell.backgroundColor = .red
    let model = presenter!.models[indexPath.row]

    cell.configure(model: model)
    
    return cell
}
Configuramos la representación visual de cada celda

Si ahora compilamos, vamos a ver qué ocurre. Spoiler, no va a funcionar, una vez hemos obtenido los datos, debemos refrescar nuestra UITableView. En el método update(movies:), cuando recibimos la información del Presenter debemos hacer un reloadData de la TableView de esta manera se mostrara la información.

extension ListOfMoviesView: ListOfMoviesUI {
    func update(movies: [ViewModel]) {
        print("Datos recibidos \(movies)")
        DispatchQueue.main.async {
            self.moviesTableView.reloadData()
        }
    }
}
Refrescamos la UITableView en el hilo principal

Una vez hemos realizado todos estos pasos, vamos a compilar (voy a borrar antes la línea donde añadimos un backgroundColor de red a la celda).

Fíjate que nuestra app ya va cogiendo forma, la lista de películas se está mostrando correctamente, pero si nuestra UI la podemos mejorar, hay algunas celdas que la descripción se solapa con la siguiente celda. Para arreglarlo nos vamos a MovieCellView y vamos a añadir este pequeño cambio en las constraints de Auto Layout:

NSLayoutConstraint.activate([
    movieImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
    movieImageView.topAnchor.constraint(equalTo: topAnchor, constant: 12),
    movieImageView.heightAnchor.constraint(equalToConstant: 200),
    movieImageView.widthAnchor.constraint(equalToConstant: 100),
    //movieImageView.bottomAnchor.constraint(equalTo: bottomAnchor),
    
    // lessThanOrEqualTo
    movieImageView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -12),
    //
    
    movieName.leadingAnchor.constraint(equalTo: movieImageView.trailingAnchor, constant: 18),
    movieName.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
    movieName.topAnchor.constraint(equalTo: movieImageView.topAnchor, constant: 24),
    
    movieDescription.leadingAnchor.constraint(equalTo: movieImageView.trailingAnchor, constant: 20),
    movieDescription.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -12),
    movieDescription.topAnchor.constraint(equalTo: movieName.bottomAnchor, constant: 8),
    
    // lessThanOrEqualTo
    movieDescription.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor, constant: -12),
])
Mejoramos las constraints de la celda

Compilamos de nuevo y vamos a ver el cambio. Perfecto, mucho mejor 👍

View Model y Mappers

Vamos a continuar mejorando nuestra app en VIPER.

Pero antes, si quieres apoyar el contenido que subo cada semana, suscríbete. De esta manera seguiré publicando contenido completamente grautito en Youtube. También comentarte que tengo un libro de Swift con varios capítulos y ejercicios prácticos totalmente en castellano. Al adquirir el libro tendrás todo el código de cada capítulo en Playgrounds y también la solución de cada ejercicio, te dejo el enlace en la descripción del video.

Te voy a lanzar una pregunta ¿no te parece extraño que estamos usando las Entities que recibimos en nuestra petición HTTP hasta la capa de presentación? es decir, estamos usando un modelo desde la capa más baja de nuestra arquitectura hasta la capa más alta.

Una buena práctica es tener modelos diferentes. La Entity que estamos pasando a nuestra celda tiene propiedades que no necesitamos en nuestra View, como la propiedad id o la propiedad votes, e incluso está conformando el protocolo Decodable, todo esto no nos sirve de nada en la View. Podemos simplificarlo y crear un modelo exclusivo para ser usado en este caso en la celda, en MovieCellView. Este modelo va a tener las propiedades que necesita la View y lo vamos a llamar ViewModel. Sobretodo no confundir con el ViewModel de la arquitectura Model-View-ViewModel, el ViewModel que estoy mencionando para nuestra app en VIPER es un modelo, una struct que va a almacenar valores en sus propiedades. Nada más.

Ahora vamos a crear el modelo con los datos que quiero representar en nuestra celda, que como hemos visto son 3. Para crear el ViewModel lo creamos en un nueva carpeta llamada ViewModel, y a nuestro modelo para simplificar tanta nomenclatrura nueva en este video, vamos a llamarlo simplemente ViewModel (aquí tendría sentido añadir un nombre más descriptivo, como MovieCellViewModel o MovieViewModel, pero para simplificar lo llamaremos ViewModel):

struct ViewModel {
    var title: String
    var overview: String
    var imageURL: String
}
ViewModel que usaremos en MovieCellView

Una vez tenemos el ViewModel que queremos representar en nuestra Celda, ¿dónde y cómo lo transformamos de Entity a ViewModel? Esta transformación la hacemos en el presenter, justo cuando recibimos los modelos del Interactor:

func onViewAppear() async {
    models = await listOfMoviesInteractor.getListOfMovies().results
    let viewModels = models.map { entity in
        ViewModel(title: entity.title,
                  overview: entity.overview,
                  imageURL: entity.imageURL)
    }
    ui?.update(movies: viewModels)
}
Transformamos el Array de models a Array de ViewModels

Si ahora intentamos compilar obtenemos un error, esto es debido a que el método update espera un Array de PopularMovieEntity en lugar de un Array de ViewModel. Para solucionarlo tenemos que:

  1. Modificar el tipo en el método del protocolo
  2. Modificar el tipo en la propiedad models (y ya de paso la llamamos viewModels)
  3. Modificar referencias a model y usar viewModel
  4. Corregir el tipo en el método que conforma ListOfMoviesView
  5. Modificar el método configure de la celda

Una vez hecho todos estos cambios debería compilar nuestra app y debería funcionar exactamente igual. Crear esta separación es muy útil para crear una app bien modularizada, sin dependencias entre modelos de otras capas y módulos.

No hemos acabado! al inicio del video te comentaba que la View debe recibir los datos directamente del Presenter, pero aquí no lo estamos cumpliendo. Esta responsabilidad de añadir "https://image.tmdb.org/t/p/w200" y concatenar con otro valor o de transformar una String al tipo URL lo debería desconocer la View, no es su responsabilidad, la View solo tiene que mostrar información. Vamos a borrarlo de aquí y vamos a añadirlo en el Presenter, justo en la transformación que hacemos de nuestro PopularMovieEntity a ViewModel, en el método onViewAppear.

Una vez lo hemos modificado, debemos tener algo parecido al siguiente código:

func onViewAppear() async {
    let models = await listOfMoviesInteractor.getListOfMovies().results
    viewModels = models.map { entity in
        ViewModel(title: entity.title,
                  overview: entity.overview,
                  imageURL:  URL(string: "https://image.tmdb.org/t/p/w200" + entity.imageURL))
    }
    ui?.update(movies: viewModels)
}
Movemos la lógica de transformar a URL y concatenar la primera parte de la URL en el Presenter

¿Si intentamos compilar qué ocurre? tenemos un error de compilación, es normal ya que uno de los tipos de nuestro ViewModel ya no es de tipo String, sino de tipo URL opcional. Vamos a arreglarlo:

struct ViewModel {
    var title: String
    var overview: String
    var imageURL: URL?
}
Cambiamos el tipo de imageURL (antes era String, ahora URL?)

Ahora mucho mejor, si compilamos nuestra app funciona perfectamente. ¿Podríamos seguir mejorando nuestra aplicación? sí, hay muchas mejoras que aún podemos aplicar a nuestra app, una de ellas es extraer la lógica de transformar el PopularMovieEntity a ViewModel en un mapper en una struct aparte, de esta manera el Presenter no tiene la responsabilidad de saber cómo transformar el Entity al ViewModel, delegamos esa responsabilidad en un tipo nuevo. Vamos a solucionarlo, dentro de la carpeta ViewModel vamos a crear un nuevo fichero llamado Mapper.

Este Mapper va a ser muy sencillo, creamos un tipo llamado Mapper y dentro de él un método con la lógica que ya teníamos en nuestro Presenter:

struct Mapper {
    func map(entity: PopularMovieEntity) -> ViewModel {
        ViewModel(title: entity.title,
                  overview: entity.overview,
                  imageURL: URL(string: "https://image.tmdb.org/t/p/w200" + entity.imageURL))
    }
}
Creamos un Mapper para mover la lógica de presentación

Ahora podemos inyectar esta dependencia, este Mapper en nuestro Presenter. Para hacerlo, lo añadimos en el init y cremos una propiedad de tipo Mapper. Ahora dentro del método onViewAppear podemos sustituirlo por:

func onViewAppear() async {
	Task {
      let models = await listOfMoviesInteractor.getListOfMovies().results
      viewModels = models.map(mapper.map(entity:))
      ui?.update(movies: viewModels)
    }
}
Usamos el Mapper en el Presenter

Mucho más limpio!

Opcional Presenter

Vamos a continuar, y vamos a la class ListOfMoviesView, aquí me gustaría hacer un cambio para que el Presenter deje de ser opcional. Podríamos dejarlo así, es decir, no es incorrecto, pero el motivo por el que lo he hecho era para que vieras dentro del Router como se creaba esta conexión de manera más visual. Aquí estamos viendo que se instancia la View y el Presenter, y a continuación se crea la conexión entre estos dos componentes. Vamos a modificarlo, vamos a nuestra ListOfMoviesView y cambiamos el init:

private let presenter: ListOfMoviesPresenter

init(presenter: ListOfMoviesPresenter) {
    self.presenter = presenter
    super.init(nibName: nil, bundle: nil)
}
Inyectamos el Presenter en el init de la View

Ahora eliminamos las referencias del Presenter que sean opcionales. Al hacerlo, si intentamos compilar tendremos un error. Esto es debido a que tenemos que hacer un último cambio en nuestro Router:

class ListOfMoviesRouter {
    func showListOfMovies(window: UIWindow?) {
        let presenter = ListOfMoviesPresenter()
        let view = ListOfMoviesView(presenter: presenter)
        
        presenter.ui = view
        //view.presenter = presenter
        
        window?.rootViewController = view
        window?.makeKeyAndVisible()
    }
}
Modificamos el Router para inyectarle el Presenter a la View

Si ahora compilamos, vemos que la app se comporta correctamente, está mostrando el listado de películas. La View tiene una dependencia que es el Presenter, es decir, para que la View funciona correctamente necesita que le inyectemos la instancia de ListOfMoviesPresenter en su inicializador.

Inyección de dependencias Swift

La verdad que hay muchas más cosas que podría explicar, pero me voy a centrar en una última que es muy importante y está relacionado con lo que acabamos de ver, la inyección de dependencias. Al inicio del video te comentaba que algunos de los componentes de VIPER se comunican con otros componentes a través de protocolos. Al hacerlo de esta manera podemos desacoplar una implementación concreta y utilizar cualquier tipo que conforme el protocolo, tiene muchas ventajas y una de ellas es poder testear componentes de VIPER por separado, por ejemplo un presenter, interactor, etc.

Cuando trabajamos con este tipo de arquitecturas, en realidad con cualquier arquitectura, no tiene porque ser solo VIPER, es muy importante trabajar en abstracciones, pero ¿esto qué significa? Ahora mismo estamos trabajando con implementaciones concretas de nuestros tipos, por ejemplo estamos usando el tipo ListOfMoviesPresenter y tipo ListOfMoviesInteractor. Vamos a coger como ejemplo este último, la implementación concreta de nuestro Interactor, esto lo podemos ver en nuestro Presenter. Lo ideal sería trabajar con abstracciones, con protocolos, de esta manera cualquier tipo que conforme el protocolo se puede sustituir e inyectar en nuestro código (tal y como vamos a ver a continuación).

Al crear estas abstracciones podemos intercambiar los tipos donde cada tipo puede tener una implementación diferente, según las necesidades que tengamos. Esto también es muy útil para realizar tests unitarios o tests de integración, sabes la escena tan famosa de Indiana Jones donde quiere intercambiar un objeto por otro?

Indiana Jones inyectando una dependencia mockeada al templo maldito
Indiana Jones inyectando una dependencia mockeada al templo maldito

Pues es justo lo que vamos hacer ahora, pero por código. Vamos a crear un protocolo que se va a implementar en dos Interactors. En realidad el Interactor que ya tenemos va a seguir funcionando igual, va a realizar la petición HTTP, pero vamos a crear otro que nos retorne datos mockeados, datos falsos que queremos mostrar en nuestra UITableView, y en este caso NO haremos una petición HTTP.

Lo primero de todo que vamos hacer es crear el protocolo, nos vamos al fichero ListOfMoviesInteractor y creamos el protocolo:

protocol ListOfMoviesInteractable {
    func getListOfMovies() async -> PopularMovieResponseEntity
}
Protocolo para crear Interactors que retornen un listado de películas

Una vez hemos creado el protocolo vamos a conformarlo en nuestro Interactor. Ahora ya podemos crear otro interactor con el comportamiento que hemos especificado antes. En lugar de realizar una petición HTTP, va a retornar un Array de modelos, su fuente de datos no es la API de movie db, es un Array que hemos escrito nosotros, con información inventada.

class ListOfMoviesInteractorMock: ListOfMoviesInteractable {
    func getListOfMovies() async -> PopularMovieResponseEntity {
        return PopularMovieResponseEntity(results: [.init(id: 0, title: "SwiftBeta", overview: "Película de un programador Swift", imageURL: "", votes: 1000),
                                                          .init(id: 1, title: "SwiftBeta1", overview: "Película de un programador Xcode", imageURL: "", votes: 1000),
                                                          .init(id: 2, title: "SwiftBeta2", overview: "Película de un programador iOS", imageURL: "", votes: 1000)])
    }
}
Creamos un mock de Interactor para retornar los valores inventados que queramos

Mientras conformemos el protocolo, nosotros podemos añadir la implementación que queramos. Vamos a continuar, ya tenemos 2 interactors que estan conformando el protocolo ListOfMoviesInteractable. Ahora vamos a nuestro presenter para hacer unos cuantos cambios.

Lo primero de todo que vamos hacer es cambiar el tipo de la propiedad y en el init de nuestro interactor, ahora será de tipo ListOfMoviesInteractable. Si compilamos nuestra app, la app sigue compilando y mostrando los datos obtenidos de la petición HTTP, el cambio viene ahora cuando escogemos el otro Interactor, el que es nuestro caballo de troya. Queremos engañar a la app para que muestre el Array que hemos creado nosotros. El cambio que hacemos es crear una instancia de nuestro InteractorMock:

init(listOfMoviesInteractor: ListOfMoviesInteractable = ListOfMoviesInteractorMock(),
     mapper: Mapper = Mapper()) {
    self.listOfMoviesInteractor = listOfMoviesInteractor
    self.mapper = mapper
}
Usamos el InteractorMock para obtener la información del Array inventado que hemos creado (en lugar de hacer la petición HTTP)

Si ahora compilamos ¿qué ocurre? se muestra la información que hemos mockeado a mano. Este ejemplo es muy potente, podríamos crear más interactors que conformaran el protocolo que hemos creado, y que en lugar de realizar la petición HTTP, o recuperaran los datos que hemos añadido nosotros, podríamos recuperar la información de las películas por ejemplo de una base de datos con Core Data. Tan solo deberíamos usar el nuevo interactor, lo podríamos intercambiar como hemos hecho en el anterior ejemplo.

En VIPER podemos crear este tipo de interfaces y trabajar con protocolos para comunicarnos con diferentes componentes, de esta manera como hemos mencionado antes podemos desacoplarnos entre capas.
El tema de inyección de dependencias que hemos visto con el interactor es muy importante para testear el código de nuestra app, y lo podemos aplicar a otros componentes como por ejemplo el Presenter.

Ahora mismo ya tenemos nuestra conexión con el Interactor a través de un protocolo. Como hemos visto nos permite ser más flexibes.

Lo que vamos hacer a continuación es crear un protocolo para nuestro Presenter, nos vamos al fichero ListOfMoviesPresenter y aquí creamos el siguiente protocolo:

protocol ListOfMoviesPresentable: AnyObject {
    var ui: ListOfMoviesUI? { get }
    var viewModels: [ViewModel] { get }
    func onViewAppear()
}

class ListOfMoviesPresenter: ListOfMoviesPresentable {
...
}
Creamos un protocolo para el Presenter

Y ahora nos vamos a nuestra View y en lugar de usar una implementación específica de nuestro Presenter, vamos a usar el protocolo. Solo vamos a modificar estas líneas de código:

private let presenter: ListOfMoviesPresentable

init(presenter: ListOfMoviesPresentable) {
    self.presenter = presenter
    super.init(nibName: nil, bundle: nil)
}	
Usamos el protocolo en la View

De esta manera podríamos pasarle otro Presenter a nuestra View tal y como hemos visto con el ejemplo del Interactor. Si ahora nos vamos a nuestro Router, podemos crear instancias y unir nuestros componentes de la siguiente manera:

func showListOfMovies(window: UIWindow?) {
    let interactor = ListOfMoviesInteractor()
    let presenter = ListOfMoviesPresenter(listOfMoviesInteractor: interactor)
    let view = ListOfMoviesView(presenter: presenter)
    
    presenter.ui = view
    //view.presenter = presenter
    
    window?.rootViewController = view
    window?.makeKeyAndVisible()
}
Inyectamos el Interactor desde el método del Router

Creamos una instancia del Interactor, para poder crear la instancia del Presenter y finalmente inyectamos el Presenter en la View.

Desde aquí también podemos inyectar tipos que conforman los protocolos que hemos creado, por ejemplo, podemos usar el Mock Interactor, el interactor con datos fake que hemos añadido nosotros, de esta manera:

func showListOfMovies(window: UIWindow?) {
    let interactor = ListOfMoviesInteractorMock()
    let presenter = ListOfMoviesPresenter(listOfMoviesInteractor: interactor)
    let view = ListOfMoviesView(presenter: presenter)
    
    presenter.ui = view
    //view.presenter = presenter
    
    window?.rootViewController = view
    window?.makeKeyAndVisible()
}
Inyectamos el mock Interactor desde el método del Router

Esta lógica de unir todos los componentes la podríamos extraer de nuestro Router y crear un tipo nuevo builder o assembler que tenga únicamente esta responsabilidad, pero no quiero complicar más el video de hoy, donde hemos explorado muchos conceptos nuevos. Lo que acabamos de ver ha sido una breve pincelada de inyección de dependencias que profundizaremos en otros videos del canal. Así que te sugiero que te suscribas 🤩.

Vamos hacer un breve repaso de lo que hemos visto en el video de hoy:

  • Hemos aprendido en qué consiste la arquitectura VIPER
  • El presenter es un mediador, conecta la view, router e interactor.
  • Los protocolos son importantes para desacoplar componentes y facilitar el testing de nuestras app.
  • Hemos lanzado un flujo completo de acción en la vista que viaja hacía el presenter,  y el presenter pide los datos al interactor. El interactor recibe los datos y los lanza hacía las capas superiores, hasta mostrarse en la vista
  • Los Entities solo los usamos para la lógica de negocio
  • Inyección de dependencias donde podemos pasar diferentes implementaciones
  • ViewModel, modelo único para la View y mappers de Entities a ViewModel

Conclusión

Hoy hemos aprendido a crear la arquitectura VIPER, hemos aprendido todos sus componentes y hemos creado una aplicación para mostrar un listado de películas. Para el siguiente video crearemos una referencia del ListOfMoviesRouter dentro del Presenter, de esta manera podremos navegar a otros módulos, a otras pantallas de nuestra app con diferentes funcionalidad. En este caso, al pulsar a una película navegaremos a un módulo nuevo de VIPER para mostrar el detalle de la película, y para hacerlo crearemos otra vez todos sus componentes.

Y hasta aquí el video de hoy!