Aprende a usar la arquitectura Model View Controller en Swift
Aprende a usar la arquitectura Model View Controller en Swift

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.

SwiftBeta

Tabla de contenido


👇 SÍGUEME PARA APRENDER SWIFTUI, SWIFT, XCODE, etc 👇
Arquitectura Model-View-Controller en Swift
Arquitectura Model-View-Controller en Swift

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)

Model-View-Controller
Contains, in alphabetical order, descriptions of design patterns, architectures, and other concepts important in Cocoa development.

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.

Estructura de carpetas con la arquitectura MVC en Swift
Estructura de carpetas con la arquitectura MVC en Swift

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:

import Foundation

final class ListOfCharactersAPIClient {
    func getListOfCharacters() async -> CharacterModelResponse {
        let url = URL(string: "https://rickandmortyapi.com/api/character")!
        let (data, _) = try! await URLSession.shared.data(from: url)
        return try! JSONDecoder().decode(CharacterModelResponse.self, from: data)
    }
}
Petición HTTP para crear el modelo de datos de nuestra app

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
struct CharacterModelResponse: Decodable {
    let results: [CharacterModel]
}
Modelo conformando el protocolo Decodable
  • CharacterModel
struct CharacterModel: Decodable {
    let name: String
    let status: String
    let species: String
    let image: String
}
Modelo con propiedades a mapear del JSON que recibieremos de nuestra petición HTTP

Nuestra carpeta Model debería contener estos 3 ficheros.

Carpeta Model
Carpeta Model

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:

import Foundation
import UIKit

class CharactersListView: UIView {
    let charactersTableView: UITableView = {
        let tableView = UITableView()
        tableView.translatesAutoresizingMaskIntoConstraints = false
        return tableView
    }()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        setupView()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setupView() {
        addSubview(charactersTableView)
        NSLayoutConstraint.activate([
            charactersTableView.leadingAnchor.constraint(equalTo: leadingAnchor),
            charactersTableView.trailingAnchor.constraint(equalTo: trailingAnchor),
            charactersTableView.topAnchor.constraint(equalTo: topAnchor),
            charactersTableView.bottomAnchor.constraint(equalTo: bottomAnchor),
        ])
    }
}
Vista en UIKit con una UITableView

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

import Foundation
import UIKit

class CharacterListCellView: UITableViewCell {
    let characterImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.translatesAutoresizingMaskIntoConstraints = false
        return imageView
    }()
    
    let characterName: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    let characterStatus: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    let characterSpecie: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0
        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(characterImageView)
        addSubview(characterName)
        addSubview(characterStatus)
        addSubview(characterSpecie)
        
        NSLayoutConstraint.activate([
            characterImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
            characterImageView.topAnchor.constraint(equalTo: topAnchor, constant: 12),
            characterImageView.heightAnchor.constraint(equalToConstant: 80),
            characterImageView.widthAnchor.constraint(equalToConstant: 80),
            
            characterName.leadingAnchor.constraint(equalTo: characterImageView.trailingAnchor, constant: 20),
            characterName.topAnchor.constraint(equalTo: characterImageView.topAnchor),
            
            characterStatus.leadingAnchor.constraint(equalTo: characterName.leadingAnchor),
            characterStatus.topAnchor.constraint(equalTo: characterName.bottomAnchor, constant: 8),
            
            characterSpecie.leadingAnchor.constraint(equalTo: characterName.leadingAnchor),
            characterSpecie.topAnchor.constraint(equalTo: characterStatus.bottomAnchor, constant: 8),
        ])
    }
}
Celda UITableViewCell para representar a cada personaje

Una vez hemos creado CharacterListCellView, vamos a registrar esta celda en nuestro UITableView de la clase CharactersListView:

let charactersTableView: UITableView = {
    let tableView = UITableView()
    tableView.translatesAutoresizingMaskIntoConstraints = false
    tableView.register(CharacterListCellView.self, forCellReuseIdentifier: "CharacterListCellView")
    return tableView
}()
Registramos la Celda que usaremos en nuestro UITableView

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:

Estructura de la carpeta Model y View
Estructura de la carpeta Model y View

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.
final class ViewController: UIViewController {
    var mainView: CharactersListView { self.view as! CharactersListView }
    
    let apiClient = ListOfCharactersAPIClient()
    // TODO
}
Creamos nuestro ViewController

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:

final class ViewController: UIViewController {
    var mainView: CharactersListView { self.view as! CharactersListView }
    
    let apiClient = ListOfCharactersAPIClient()
            
    override func loadView() {
        view = CharactersListView()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        
        title = "Characters"
        
        Task {
            let characters = await apiClient.getListOfCharacters().results
            print("Characters \(characters)")
        }
    }
}
Realizamos la petición HTTP al cargar nuestro View Controller

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.

import Foundation
import UIKit

final class ListOfCharactersTableViewDataSource: NSObject, UITableViewDataSource {
    private let tableView: UITableView
    
    private(set) var characters: [CharacterModel] = [] {
        didSet {
            DispatchQueue.main.async {
                self.tableView.reloadData()
            }
        }
    }
    
    init(tableView: UITableView, characters: [CharacterModel] = []) {
        self.tableView = tableView
        self.characters = characters
    }
    
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return characters.count
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "CharacterListCellView", for: indexPath) as! CharacterListCellView
        
        let character = characters[indexPath.row]
        cell.backgroundColor = .blue
        
        return cell
    }
    
    func set(characters: [CharacterModel]) {
        self.characters = characters
    }
}
UITableViewDataSource
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.

import UIKit

final class CharactersListViewController: UIViewController {
    var mainView: CharactersListView { self.view as! CharactersListView }
    
    private var tableViewDataSource: ListOfCharactersTableViewDataSource? // 1
    
    let apiClient = ListOfCharactersAPIClient()
            
    override func loadView() {
        view = CharactersListView()
        
        tableViewDataSource = ListOfCharactersTableViewDataSource(tableView: mainView.charactersTableView) // 2
        
        mainView.charactersTableView.dataSource = tableViewDataSource // 3
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        
        title = "Characters"
        
        Task {
            let characters = await apiClient.getListOfCharacters().results
            print("Characters \(characters)")
            tableViewDataSource?.set(characters: characters) // 4
        }
    }
}
Proporcionamos modelos a nuestro UITableViewDataSource para mostrar en cada celda del UITableView
  1. Creamos la propiedad de tipo ListOfCharactersTableViewDataSource
  2. Creamos una instancia, pasándole la dependencia de nuestro UITableView
  3. asignamos la instancia que acabamos de crear en 2 al dataSource del UITableView
  4. 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:

func configure(_ model: CharacterModel) {
    self.characterName.text = model.name
    self.characterSpecie.text = model.species
    self.characterStatus.text = model.status
}
Asignamos un modelo a las subvistas de nuestra UITableVIewCell

Ahora vamos a nuestro ListOfCharactersTableViewDataSource y vamos a actualizar la línea donde cambiábamos el backgroundColor a .blue:

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    let cell = tableView.dequeueReusableCell(withIdentifier: "CharacterListCellView", for: indexPath) as! CharacterListCellView
    
    let character = characters[indexPath.row]
    cell.configure(character)

    return cell
}
Por cada modelo llamamos a la configuración de la celda

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

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 tenemos la ULR nos vamos al Projecto y clickamos en la sección Package Dependencies

Añadimos dependencia con Swift Package Manager en Xcode
Añadimos dependencia con Swift Package Manager en Xcode

Aquí damos al button + y añadimos la URL del repositorio de Github

Añadimos la dependencia KingFisher
Añadimos la dependencia KingFisher

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.

Dependencia KingFisher añadida a Xcode
Dependencia KingFisher añadida a Xcode

Volvemos a nuestra celda y vamos hacer dos cosas:

  1. Importar Kingfisher
import Foundation
import UIKit
import Kingfisher
Importamos KingFisher en nuestra UITableViewCell

2. Añadimos una línea al método que nos configura la celda, y el método quedaría de la siguiente manera:

func configure(_ model: CharacterModel) {
        self.characterName.text = model.name
        self.characterSpecie.text = model.species
        self.characterStatus.text = model.status
        self.characterImageView.kf.setImage(with: URL(string: model.image))
}
Asignamos la URL de la imagen de nuestro modelo al UIImageView

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:

import Foundation
import UIKit

final class ListOfCharactersTableViewDelegate: NSObject, UITableViewDelegate {
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        120
    }
}
Conformamos el protocolo UITableViewDelegate

Y una vez creado, vamos a crear una propiedad e instanciarlo para asignarselo a nuestro UITableView.

import UIKit

final class CharactersListViewController: UIViewController {
    var mainView: CharactersListView { self.view as! CharactersListView }
    
    private var tableViewDataSource: ListOfCharactersTableViewDataSource?
    private var tableViewDelegate: ListOfCharactersTableViewDelegate?   // 1
    
    let apiClient = ListOfCharactersAPIClient()
            
    override func loadView() {
        view = CharactersListView()
        tableViewDelegate = ListOfCharactersTableViewDelegate() // 2
        
        tableViewDataSource = ListOfCharactersTableViewDataSource(tableView: mainView.charactersTableView)
        
        mainView.charactersTableView.dataSource = tableViewDataSource
        mainView.charactersTableView.delegate = tableViewDelegate // 3
    }
    // code
}
Creamos una instancia de ListOfCharactersTableViewDelegate en nuestro ViewController

En este caso, vamos a ver paso por paso lo que hemos añadido:

  1. Propiedad de tipo ListOfCharactersTableViewDelegate
  2. Creamos una instancia de ListOfCharactersTableViewDelegate
  3. Asignamos el delegate al UITableView

Ahora, toca compilar para ver el resultado final.

Simulador de Xcode mostrando todos los personajes de la serie Rick and Morty
Simulador de Xcode mostrando todos los personajes de la serie Rick and Morty

Perfecto! Nuestra carpeta Controller tiene los siguientes ficheros

Estructura de la carpeta Controller
Estructura de la carpeta Controller

Y podríamos agrupar la carpeta Model, View y Controller en otra. A la que vamos a llamar ListOfCharacters

Creamos una carpeta que contenga el MVC de nuestra pantalla
Creamos una carpeta que contenga el MVC de nuestra pantalla

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.

class CharacterDetailView: UIView { // 1
    let characterImageView: UIImageView = {
        let imageView = UIImageView()
        imageView.translatesAutoresizingMaskIntoConstraints = false
        return imageView
    }()
    
    let characterName: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    let characterStatus: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    let characterSpecie: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    override init(frame: CGRect) { // 2
        super.init(frame: .zero)
        setupViews()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func setupViews() {
        backgroundColor = .white // 3
        
        addSubview(characterImageView)
        addSubview(characterName)
        addSubview(characterStatus)
        addSubview(characterSpecie)
        
        NSLayoutConstraint.activate([ // 4
            characterImageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 12),
            characterImageView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor, constant: 12),
            characterImageView.heightAnchor.constraint(equalToConstant: 200),
            characterImageView.widthAnchor.constraint(equalToConstant: 200),
            
            characterName.leadingAnchor.constraint(equalTo: characterImageView.trailingAnchor, constant: 20),
            characterName.topAnchor.constraint(equalTo: characterImageView.topAnchor),
            
            characterStatus.leadingAnchor.constraint(equalTo: characterName.leadingAnchor),
            characterStatus.topAnchor.constraint(equalTo: characterName.bottomAnchor, constant: 8),
            
            characterSpecie.leadingAnchor.constraint(equalTo: characterName.leadingAnchor),
            characterSpecie.topAnchor.constraint(equalTo: characterStatus.bottomAnchor, constant: 8),
        ])
    }
    
    func configure(_ model: CharacterModel) {
        self.characterName.text = model.name
        self.characterSpecie.text = model.species
        self.characterStatus.text = model.status
        self.characterImageView.kf.setImage(with: URL(string: model.image))
    }
}
Creamos una subclase de UIView para crear la vista detalle de un personaje

Es la misma vista, pero hemos cambiado 4 partes:

  1. Subclase de UIView
  2. Usamos el inicializador acorde a UIView (en lugar de UITableViewCell)
  3. Añadimos un backgroundColor de white a la vista
  4. 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

Creamos una nueva estructura de carpetas para seguir el MVC
Creamos una nueva estructura de carpetas para seguir el MVC

Ahora toca crear el ViewController. Pulsamos COMMAND+N y creamos un ViewController llamado CharacterDetailViewController.

Y va a tener el siguiente código:

import UIKit

final class CharacterDetailViewController: UIViewController {

    var mainView: CharacterDetailView { self.view as! CharacterDetailView }
    
    override func loadView() {
        view = CharacterDetailView()
    }
    
    init(characterDetailModel: CharacterModel) {
        super.init(nibName: nil, bundle: nil)
        mainView.configure(characterDetailModel)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
    }
}
ViewController para mostrar la información con el detalle del personaje

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:

import Foundation
import UIKit

final class ListOfCharactersTableViewDelegate: NSObject, UITableViewDelegate {
    var didTapOnCell: ((Int) -> Void)?
    
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        120
    }
    
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        didTapOnCell?(indexPath.row)
    }
}
Creamos un nuevo método del UITableViewDelegate para saber cuándo una celda se ha pulsado

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:

tableViewDelegate?.didTapOnCell = { [weak self] index in
    print("Index \(index)")
    // Present New View Controller
    guard let dataSource = self?.tableViewDataSource else {
        return
    }
    let characterModel = dataSource.characters[index]
    let characterDetailViewController = CharacterDetailViewController(characterDetailModel: characterModel)
    self?.present(characterDetailViewController,
                 animated: true)
}
Navegamos al CharacterDetailViewController al puslar una de las celdas del UITableView

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.