Ciclo de vida de un view controller: viewDidLoaad, viewWillAppear, etc
Ciclo de vida de un view controller: viewDidLoaad, viewWillAppear, etc

Ciclo de Vida de un ViewController

El ciclo de vida de un view controller es desde que se va presentar la view hasta que la view desaparece de la jerarquía de vistas. Durante este ciclo, el view controller recibe varias notificaciones, vamos a explicarlas una a una.

SwiftBeta

Tabla de contenido

Ciclo de vida de un ViewController

Hoy en SwiftBeta vamos a entender el ciclo de vida de un ViewController y vamos a aprender a liberar sus recursos cuando ya no los tenemos en la jerarquía de vistas. Como hemos ido comentando durante la serie de UIKit, un ViewController es la parte fundamental del framework UIKit. Para mostrar las vistas de nuestra app usamos ViewControllers y estos tienen un ciclo de vida. Es decir, un ViewController aparece en el flujo de nuestra app, lo usamos durante un periodo de tiempo y cuando hemos dejado de usarlo, ya sea por que hemos navegado a otra vista de nuestra app, el ViewController se libera, también liberando todos los recursos que estaba consumiendo.
Es muy importante dejar claro que un móvil tiene unos recursos limitados, y uno de ellos es la memoria. Debemos entender este ciclo de vida e impedir que se generen retain cycles, ya que si nuestra app acaba consumiendo mucha memoria, el sistema operativo nos la cerrará de golpe, creando una mala experiencia para nuestros users.

Hoy vamos a ver una serie de métodos que se ejecutan en el ciclo de vida de nuestro ViewController.

Pero antes, apoya el canal suscribiendote, de esta manera seguiré subiendo un video nuevo cada semana, con las ultimas novedades de Swift, SwiftUI y Xcode.

Vamos a crear un proyecto de cero en Xcode y vamos a ver ejemplos prácticos,

Creamos proyecto en Xcode

Lo primero de todo es crear un proyecto en Xcode. Al crear el proyecto seleccionar como interface Storyboard, ya que vamos a trabajar con el framework UIKit.

Una vez creado el proyecto, si vamos a nuestro listado de ficheros, allí vemos el AppDelegate, SceneDelegate y ViewController. Vamos al ViewController.

Dentro del ViewController, vemos que hay un override de un método llamado viewDidLoad. Este método se hereda de UIViewController, es decir nuestro ViewController es una subclase de UIViewController.

Vamos a ver la documentación de UIViewController, para ello pulsamos COMMAND y clickamos en UIViewController. Aquí dentro podemos ver una serie de inicializadores, propiedades y métodos que podemos sobreescribir cuando desarrollemos nuestra app. Y podemos ver el famoso viewDidLoad, exacto el que estábamos haciendo un override en nuestro ViewController.

Volvemos a nuestro ViewController y vamos a ver algunos métodos que se llaman durante el ciclo de vida de nuestro ViewController. Todos los método que vamos a ver se llaman por orden:

viewDidLoad

El primero de todos que vamos a ver es el viewDidLoad, este método se llama para notificar al ViewController de que su vista se ha cargado en memoria. Dentro de este método podemos añadir lógica, añadir subvistas, añadir constraints, etc. Más tarde crearemos un ejemplo real.

Voy a añadir un print dentro de este método.

override func viewDidLoad() {
    super.viewDidLoad()
    print("1. viewDidLoad \(self.debugDescription)")
}
Método viewDidLoad de un ViewController

viewWillAppear

El segundo método que se llama, es el viewWillAppear y es cuando la vista está preparada y se va a mostrar en nuestro ViewController.

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    print("2. viewWillAppear \(self.description))")
}
Método viewWillAppear de un ViewController

viewWillLayoutSubviews

Continuamos con el viewWillLayoutSubviews, este método se llama para notificar al ViewController de que la vista está apunto de posicionar sus subvistas. Luego veremos un ejemplo real, pero si rotaramos nuestro device, este método se llamaría.

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    print("3. viewWillLayoutSubviews \(self.description)")
}
Método viewWillLayoutSubviews de un ViewController

viewDidLayoutSubviews

Otro método es el viewDidLayoutSubviews, este método se llama para notificar al ViewController de que la vista acaba de posicionar todas sus subvistas.

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    print("4. viewDidLayoutSubviews \(self.description)")
}
Método viewDidLayoutSubviews de un ViewController
Un ejemplo muy claro de cuando se llaman estos dos últimos métodos es cuando rotamos nuestro device, si la app soporta el modo landscape, las vistas se tienen que distribuir por el nuevo tamaño de pantalla. Y dentro de este proceso se llama a los dos métodos anteriores

viewDidAppear

Vamos a continuar, ahora toca el método viewDidAppear. Este método se llamará cuando la vista ya se ha cargado en el ViewController y se está mostrando al user

override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    print("5. viewDidAppear \(self.description)")
}
Método viewDidAppear de un ViewController

viewWillDisappear

Todos los que hemos visto antes son al cargar un ViewController. Pero también podemos controlar y ejecutar código cuando un ViewController esté apunto de desaparecer. Esto lo veremos a continuación en un ejemplo real. Pero imagina que que dimisseas un ViewController de tu jerarquía de vistas, entonces se llamaría al método viewWillDisappear para notificar al ViewController de que la vista está apunto de ser eliminada de la jerarquía de vistas.

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    print("6. viewWillDisappear \(self.description)")
}
Método viewWillDisappear de un ViewController

viewDidDisappear

Y para finalizar, justo después de que se ejecutara el código de viewWillAppear, se ejecutaría el método viewDidDisappear. Con esto recibiríamos una notificación al ViewController de que la vista ya ha sido eliminada de la jerarquía de vistas.

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    print("7. viewDidDisappear \(self.description)")
}
Método viewDidDisappear de un ViewController

Si compilamos ahora, vamos a ver unos cuantos prints por consola. Se están mostrando todos menos viewWillDisappear y viewDidDisappear. Vamos a añadir una subvista para poder el flujo completo del ciclo de vida de nuestro ViewController.


Añadimos subvistas a nuestro ViewController

Ahora vamos a añadir un UIButton a nuestro ViewController. Para hacerlo vamos ha crearlo por código, como ya hemos visto en otros videos de la serie de UIKit.

class ViewController: UIViewController {
    private lazy var swiftBetaButton: UIButton = {
            var configuration = UIButton.Configuration.filled()
            configuration.title = "Suscríbete a SwiftBeta"
            configuration.titleAlignment = .center
            configuration.subtitle = "Apoya el canal"
            configuration.image = UIImage(systemName: "play.circle.fill")
            configuration.imagePadding = 12
            configuration.imagePlacement = .bottom
            configuration.buttonSize = .large
            
            let button = UIButton(type: .system, primaryAction: UIAction(handler: { action in
                self.presentCurrentViewController()
              }))
            button.translatesAutoresizingMaskIntoConstraints = false
            button.configuration = configuration
            
            return button
        }()


    override func viewDidLoad() {
        super.viewDidLoad()
        print("viewDidLoad 1")
        view.addSubview(swiftBetaButton)
        
        NSLayoutConstraint.activate([
            swiftBetaButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            swiftBetaButton.centerYAnchor.constraint(equalTo: view.centerYAnchor),
        ])
    }
    
    func presentCurrentViewController() {
        // TODO
    }
Código para añadir un UIButton a un ViewController

Este UIButton nos va a servir para navegar a otro ViewController

¿Cuando se ejecuta viewWillDisappear y viewDidDisappear?

En el método que hemos creado hace un momento, el presentCurrentViewController vamos a presentar nuestro ViewController. Es decir, vamos a presentar el único ViewController que tenemos, cada vez que pulsemos en nuestro código.

func presentCurrentViewController() {
    self.present(ViewController(), animated: true)
}
Presentamos un ViewController

Y para que sea más fácil entender que cada vez presentamos una instancia nueva de nuestro ViewController, voy a asignarle un backgroundColor de forma random. Así que dentro del viewDidLoad añadimos la siguiente línea de código:

override func viewDidLoad() {
    super.viewDidLoad()
    print("viewDidLoad 1")
    view.backgroundColor = [.systemRed,
                            .systemBlue,
                            .systemCyan,
                            .systemMint,
                            .systemPink,
                            .systemTeal,
                            .systemBrown].randomElement()
	// código
Array de colores donde seleccionamos uno al azar

Si ahora compilamos y pulsamos el UIButton. Vemos como se van presentando UIViewControllers a nuestra jerarquía de vistas. Pero si te fijas aunque vayamos presentando ViewController los métodos viewWillDisappear y viewDidDiappear no se están llamando. Esto es por que todos estos ViewControllers aún están en la jerarquía de vistas. Si quieres eliminarlos, lo único que tienes que hacer es dismissearlos. Al cerrar un ViewController, vemos como aparece por consola el siguiente mensaje:

viewWillDisappear 1
viewDidDisappear 1

Hasta aquí todo bien, pero cómo podemos saber que los recursos de nuestro ViewController se están liberando correctamente?

deinit

Podemos usar un método llamado deinit. Igual que tenemos los init para inicializar y crear instancias de nuestras clases, deinit se llama cuando el ViewController ha sido liberado en memoria. Vamos a verlo, justo vamos a añadir un método llamado deinit al principio de nuestro ViewController:

class ViewController: UIViewController {
    deinit {
        print("🧹")
    }
    // código
}
Método deinit de un ViewController

Si ahora compilamos y vamos presentando ViewControllers en nuestra jerarquía de vistas, al dimissear uno de ellos ¿qué ocurre? pues que el método deinit no se está ejecutando, pero ¿por qué? por que tenemos un retain cycle. Si no sabes lo que es un retain cycle lo explico en este video super interesante, y te invito a que le eches un vistazo. Ya que seguro aprendes cosas nuevas para tu carrera como iOS Developer.

Lo que debemos hacer ahora es romper este retain cycle y así liberar los recursos de nuestro ViewController, para hacerlo, yo ya tengo identificado donde ocurre, es decir, donde tenemos el problema. Tenemos que ir a la configuración de nuestro swiftBetaButton y aquí tan solo tenemos  poner [weak self] en el closure de la acción que se ejecuta al pulsar el UIButton:

let button = UIButton(type: .system, primaryAction: UIAction(handler: { [weak self] action in
                self?.presentCurrentViewController()
              }))
Rompemos el retain cycle de nuestro ViewController

Ahora si repetimos el mismo proceso de antes, vamos a mostrar varios ViewController y al dismisear uno de ellos obtenemos el siguiente mensaje por consola:

viewWillDisappear 1
viewDidDisappear 1
🧹

Es importante entender que, aunque estemos dismiseando un ViewController, esto no significa que se esté liberando.

Antes de acabar con el video, me gustaría ver un ejemplo real de llamadas a viewWillLayoutSubviews y viewDidLayoutSubviews. En lugar de rotar el device para que se ejecuten, vamos hacer una cosa muy chula

¿Cuando se ejecuta viewWillLayoutSubviews y viewDidLayoutSubviews?

Vamos a crear una de las constraints de UIButton, y vamos a modificarla para que cuando se pulse el UIButton se modifique la constraint centerYAnchor. Para hacerlo voy a crear una propiedad:

var buttonConstraint: NSLayoutConstraint?

Voy a modificar el viewDidLoad

    override func viewDidLoad() {
        super.viewDidLoad()
        print("1. viewDidLoad \(self.description)")
        view.backgroundColor = [.systemRed,
                                .systemBlue,
                                .systemCyan,
                                .systemMint,
                                .systemPink,
                                .systemTeal,
                                .systemBrown].randomElement()
        view.addSubview(swiftBetaButton)
        
        buttonConstraint = swiftBetaButton.centerYAnchor.constraint(equalTo: view.centerYAnchor)
        NSLayoutConstraint.activate([
            swiftBetaButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            buttonConstraint!,
        ])
    }
Ejemplo para mostrar cuándo se ejecuta el método viewWillLayoutSubviews

y finalmente actualizamos el método que se lanza cuando pulsamos el UIButton.

            let button = UIButton(type: .system, primaryAction: UIAction(handler: { [weak self] action in
                //self?.presentCurrentViewController()
                self?.changeConstraintValue()
              }))
Cambiamos el método que ejecutaba nuestro UIButton
    func changeConstraintValue() {
        view.removeConstraint(buttonConstraint!)
        buttonConstraint = swiftBetaButton.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: 100)
        NSLayoutConstraint.activate([
            swiftBetaButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            buttonConstraint!,
        ])
    }
Modificamos la constraint

si ahora compilamos y probamos el código, vemos que cada vez que pulsamos el UIButton, se llaman a los métodos viewWillLayoutSubviews y viewDidLayoutSubviews

Para acabar, me gustaría explicar rápidamente que un ViewController tiene más método que se llaman cuando pasan ciertas acciones

didReceiveMemoryWarning

Una de ellas es cuando nuestra app está consumiendo demasiada memoria. Antes de que el sistema cierre nuestra app, hay un método llamado didReceiveMemoryWarning que es llamado. Aquí podemos aplicar lógica para que antes de que se cierre la app se ejecute. Vamos a poner un print

override func didReceiveMemoryWarning() {
    print("did Receive Memory Warning")
}
Método de nuestro ViewController que se ejecuta cuando recibimos un Memory Warning

Vamos a compilar y vamos a ver cómo podemos ejecutar este método. Desde el simulador, vamos a una de las opciones del menu Debug -> Simulate Memory Warning.

Simulamos un Memory Warning en Xcode
Simulamos un Memory Warning en Xcode

Al seleccionar esta opción se ejecuta el método didReceiveMemoryWarning de nuestro ViewController, printando el mensaje que habíamos especificado.

Conclusión

Hoy hemos aprendido sobre el ciclo de vida de un UIViewController. Es importante tener esto en cuenta para crear apps que sean eficientes y vayan liberando los recursos que ya no necesitan. Hemos aprendido a usar los métodos que se llaman para cargar la View y también los método que se llaman cuando la View va a desaparecer de la jerarquía de vistas. También hemos visto dos métodos que se llaman cuando las subvistas de una View se tienen que redistribuir en la vista padre.
Para finalizar, hemos visto otro método que notifica al ViewController cuando hay un memory warning.

Si quieres seguir aprendiendo sobre SwiftUI, Swift, Xcode, o cualquier tema relacionado con el ecosistema Apple