La forma correcta de compartir el estado entre los controladores de vista rápida

Lo que vas a crear

Hace unos años, cuando todavía era empleado en una consultoría móvil, trabajé en una aplicación para un gran banco de inversión. Las grandes empresas, especialmente los bancos, generalmente tienen procesos implementados para garantizar que su software sea seguro, robusto y mantenible.

Parte de este proceso involucró el envío del código de la aplicación que escribí a un tercero para su revisión. Eso no me molestó, porque pensé que mi código era impecable y que la compañía de revisión diría lo mismo.

Cuando su respuesta regresó, el veredicto fue diferente de lo que pensé. Aunque dijeron que la calidad del código no era mala, señalaron el hecho de que el código era difícil de mantener y de probar (las pruebas de unidades no eran muy populares en el desarrollo de iOS en ese entonces).

Deseché su juicio, pensando que mi código era excelente y que no había forma de mejorarlo. Ellos simplemente no deben entenderlo.!

Tenía la típica arrogancia de los desarrolladores: a menudo pensamos que lo que hacemos es excelente y que otros no lo entienden. 

En retrospectiva, estaba equivocado. Poco después, comencé a leer sobre algunas de las mejores prácticas. A partir de entonces, los problemas en mi código comenzaron a sobresalir como un pulgar adolorido. Me di cuenta de que, como muchos desarrolladores de iOS, había sucumbido a algunas trampas clásicas de malas prácticas de codificación..

Lo que la mayoría de los desarrolladores de iOS se equivocan

Una de las malas prácticas de desarrollo de iOS más comunes surge cuando se pasa el estado entre los controladores de vista de una aplicación. Yo mismo he caído en esta trampa en el pasado..

La propagación del estado a través de los controladores de vista es vital en cualquier aplicación de iOS. A medida que sus usuarios navegan por las pantallas de su aplicación e interactúan con ella, debe mantener un estado global que haga un seguimiento de todos los cambios que el usuario realiza en los datos..

Y aquí es donde la mayoría de los desarrolladores de iOS buscan la solución obvia, pero incorrecta: el patrón singleton.

El patrón de singleton es muy rápido de implementar, especialmente en Swift, y funciona bien. Solo tiene que agregar una variable estática a una clase para mantener una instancia compartida de la clase en sí, y listo..

clase Singleton estático dejado compartido = Singleton ()

Entonces es fácil acceder a esta instancia compartida desde cualquier lugar en su código:

vamos singleton = Singleton.shared

Por esta razón, muchos desarrolladores creen que encontraron la mejor solución al problema de la propagación del estado. Pero estan equivocados.

El patrón singleton en realidad se considera un anti-patrón. Ha habido muchas discusiones sobre esto en la comunidad de desarrollo. Por ejemplo, vea esta pregunta de desbordamiento de pila.

En pocas palabras, los singletons crean estos problemas:

  • Introducen muchas dependencias en sus clases, lo que dificulta el cambio en el futuro..
  • Hacen que el estado global sea accesible a cualquier parte de su código. Esto puede crear interacciones complejas que son difíciles de rastrear y causar muchos errores inesperados.
  • Hacen que tus clases sean muy difíciles de evaluar, ya que no puedes separarlas de un singleton fácilmente.

En este punto, algunos desarrolladores piensan: “Ah, tengo una solución mejor. Usaré el AppDelegate en lugar".

El problema es que la AppDelegate clase en aplicaciones iOS se accede a través de la UIApplicación instancia compartida:

deja appDelegate = UIApplication.shared.delegate

Pero la instancia compartida de UIApplicación es en sí un singleton. Así que no has resuelto nada.!

La solución a este problema es la inyección de dependencia. La inyección de dependencia significa que una clase no recupera ni crea sus propias dependencias, sino que las recibe del exterior..

Para ver cómo usar la inyección de dependencias en aplicaciones iOS y cómo puede habilitar el intercambio de estado, primero debemos volver a visitar uno de los patrones arquitectónicos fundamentales de las aplicaciones iOS: el patrón Modelo-Vista-Controlador.

Extendiendo el patrón MVC

El patrón MVC, en pocas palabras, establece que hay tres capas en la arquitectura de una aplicación iOS:

  • La capa modelo representa los datos de una aplicación..
  • La capa de visualización muestra información en la pantalla y permite la interacción..
  • La capa del controlador actúa como pegamento entre las otras dos capas, moviendo datos entre ellas.

La representación habitual del patrón MVC es algo como esto:

El problema es que este diagrama es incorrecto..

Este "secreto" se oculta a simple vista en un par de líneas en la documentación de Apple:

“Uno puede combinar los roles MVC desempeñados por un objeto, haciendo que un objeto, por ejemplo, cumpla con los roles del controlador y de la vista, en cuyo caso, se llamaría controlador de la vista. De la misma manera, también puede tener objetos de controlador de modelo ".

Muchos desarrolladores piensan que los controladores de vista son los únicos que existen en una aplicación de iOS. Por esta razón, una gran cantidad de código termina siendo escrito dentro de ellos por falta de un lugar mejor. Esto es lo que hace que los desarrolladores utilicen singletons cuando necesitan propagar el estado: parece ser la única solución posible.

A partir de las líneas citadas anteriormente, está claro que podemos agregar una nueva entidad a nuestra comprensión del patrón MVC: el controlador modelo. Los controladores modelo se ocupan del modelo de la aplicación, cumpliendo los roles que el modelo en sí mismo no debería cumplir. Así es como debe verse el esquema anterior:

El ejemplo perfecto de cuando un controlador modelo es útil es para mantener el estado de la aplicación. El modelo debe representar solo los datos de su aplicación. El estado de la aplicación no debe ser su preocupación..

Este mantenimiento del estado generalmente termina dentro de los controladores de vista, pero ahora tenemos un lugar nuevo y mejor para ponerlo: un controlador modelo. Este controlador modelo se puede pasar a los controladores de vista a medida que aparecen en la pantalla a través de la inyección de dependencia.

Hemos resuelto el anti-patrón singleton. Veamos nuestra solución en la práctica con un ejemplo..

Estado de propagación a través de controladores de vista que utilizan inyección de dependencia

Vamos a escribir una aplicación simple para ver un ejemplo concreto de cómo funciona esto. La aplicación mostrará su cita favorita en una pantalla y le permitirá editarla en una segunda pantalla..

Esto significa que nuestra aplicación necesitará dos controladores de vista, que deberán compartir el estado. Después de ver cómo funciona esta solución, puede expandir el concepto a aplicaciones de cualquier tamaño y complejidad..

Para empezar, necesitamos un tipo de modelo para representar los datos, que en nuestro caso es una cita. Esto se puede hacer con una estructura simple:

struct Quote let text: String let author: String

El controlador modelo

Entonces necesitamos crear un controlador modelo que mantenga el estado de la aplicación. Este controlador modelo debe ser una clase. Esto se debe a que necesitaremos una sola instancia que pasaremos a todos nuestros controladores de vista. Los tipos de valor como estructuras se copian cuando los pasamos, por lo que claramente no son la solución correcta.

Todo lo que nuestro controlador modelo necesita en nuestro ejemplo es una propiedad donde puede mantener la cotización actual. Pero, por supuesto, en aplicaciones más grandes, los controladores de modelo pueden ser más complejos que esto:

class ModelController var quote = Cita (texto: "Dos cosas son infinitas: el universo y la estupidez humana; y no estoy seguro acerca del universo", autor: "Albert Einstein")

Le asigné un valor predeterminado a la citar propiedad, por lo que ya tendremos algo que mostrar en la pantalla cuando se inicie la aplicación. Esto no es necesario, y usted podría declarar que la propiedad es una inicialización opcional para nulo, Si desea que su aplicación se inicie con un estado en blanco.

Crear la interfaz de usuario

Ahora tenemos el controlador modelo, que contendrá el estado de nuestra aplicación. A continuación, necesitamos los controladores de vista que representarán las pantallas de nuestra aplicación.

Primero, creamos sus interfaces de usuario. Así es como se ven los dos controladores de vista dentro del guión gráfico de la aplicación..

La interfaz del primer controlador de vista está formada por un par de etiquetas y un botón, junto con simples restricciones de diseño automático. (Puedes leer más sobre el diseño automático aquí en Envato Tuts +.)

La interfaz del segundo controlador de vista es la misma, pero tiene una vista de texto para editar el texto de la cita y un campo de texto para editar el autor..

Los dos controladores de vista están conectados por un solo segmento de presentación modal, que se origina en el Editar cita botón.

Puede explorar la interfaz y las restricciones de los controladores de vista en el repositorio de GitHub.

Code a View Controller con inyección de dependencia

Ahora necesitamos codificar nuestros controladores de vista. Lo importante que debemos tener en cuenta aquí es que necesitan recibir la instancia del controlador modelo desde el exterior, a través de la inyección de dependencia. Así que necesitan exponer una propiedad para este propósito..

var modelController: ModelController!

Podemos llamar a nuestro primer controlador de vista. QuoteViewController. Este controlador de vista necesita un par de salidas a las etiquetas para la cita y el autor en su interfaz.

clase QuoteViewController: UIViewController @IBOutlet weak var quoteTextLabel: UILabel! @IBOutlet weak var quoteAuthorLabel: UILabel! var modelController: ModelController! 

Cuando este controlador de vista aparece en la pantalla, rellenamos su interfaz para mostrar la cotización actual. Ponemos el código para hacer esto en el controlador. viewWillAppear (_ :) método.

clase QuoteViewController: UIViewController @IBOutlet weak var quoteTextLabel: UILabel! @IBOutlet weak var quoteAuthorLabel: UILabel! var modelController: ModelController! anular func viewWillAppear (_ animated: Bool) super.viewWillAppear (animated) let quote = modelController.quote quoteTextLabel.text = quote.text quoteAuthorLabel.text = quote.author

Podríamos haber puesto este código dentro de la viewDidLoad () Método en su lugar, que es bastante común. El problema, sin embargo, es que viewDidLoad () se llama solo una vez, cuando se crea el controlador de vista. En nuestra aplicación, necesitamos actualizar la interfaz de usuario de QuoteViewController Cada vez que aparece en la pantalla. Esto se debe a que el usuario puede editar la cotización en la segunda pantalla. 

Por eso usamos el viewWillAppear (_ :) método en lugar de viewDidLoad (). De esta manera podemos actualizar la interfaz de usuario del controlador de vista cada vez que aparezca en la pantalla. Si desea saber más sobre el ciclo de vida de un controlador de vista y todos los métodos a los que se llama, escribí un artículo en el que se detallan todos..

El controlador de vista de edición

Ahora necesitamos codificar el segundo controlador de vista. Vamos a llamar a este EditViewController.

clase EditViewController: UIViewController @IBOutlet weak var textView: UITextView! @IBOutlet weak var textField: UITextField! var modelController: ModelController! anular func viewDidLoad () super.viewDidLoad () let quote = modelController.quote textView.text = quote.text textField.text = quote.author

Este controlador de vista es como el anterior:

  • Tiene puntos de venta para la vista de texto y el campo de texto que el usuario utilizará para editar la cita..
  • Tiene una propiedad para la inyección de dependencia de la instancia del controlador de modelo.
  • Rellena su interfaz de usuario antes de aparecer en pantalla..

En este caso, utilicé el viewDidLoad () Método porque este controlador de vista aparece en la pantalla una sola vez..

Compartiendo el estado

Ahora debemos pasar el estado entre los dos controladores de vista y actualizarlo cuando el usuario edite la cotización.

Pasamos el estado de la aplicación en el preparar (para: remitente :) método de QuoteViewController. Este método es activado por el segmento conectado cuando el usuario pulsa en el Editar cita botón.

clase QuoteViewController: UIViewController @IBOutlet weak var quoteTextLabel: UILabel! @IBOutlet weak var quoteAuthorLabel: UILabel! var modelController: ModelController! anular func viewWillAppear (_ animated: Bool) super.viewWillAppear (animated) let quote = modelController.quote quoteTextLabel.text = quote.text quoteAuthorLabel.text = quote.author override func prepare (for segue: UIStoryboardSegue, sender: Any? ) si se deja editViewController = segue.destination as? EditViewController editViewController.modelController = modelController

Aquí pasamos adelante la instancia de la Controlador de modelo Eso mantiene el estado de la aplicación. Aquí es donde la inyección de dependencia para el EditViewController sucede.

En el EditViewController, tenemos que actualizar el estado a la nueva cotización antes de volver al controlador de vista anterior. Podemos hacer esto en una acción conectada al Salvar botón:

clase EditViewController: UIViewController @IBOutlet weak var textView: UITextView! @IBOutlet weak var textField: UITextField! var modelController: ModelController! anular func viewDidLoad () super.viewDidLoad () let quote = modelController.quote textView.text = quote.text textField.text = quote.author @IBAction func save (_ sender: AnyObject) let newQuote = Quote (text: textView.text, autor: textField.text!) modelController.quote = newQuote dismiss (animated: true, complete: nil)

Inicialice el controlador de modelo

Ya casi hemos terminado, pero es posible que haya notado que todavía nos falta algo: el QuoteViewController pasa el Controlador de modelo al EditViewController A través de la inyección de dependencia. Pero quien le da esta instancia a la QuoteViewController ¿en primer lugar? Recuerde que al usar la inyección de dependencia, un controlador de vista no debe crear sus propias dependencias. Estas tienen que venir del exterior..

Pero no hay un controlador de vista antes de la QuoteViewController, Porque este es el primer controlador de vista de nuestra aplicación. Necesitamos algún otro objeto para crear el Controlador de modelo instancia y pasarlo a la QuoteViewController.

Este objeto es el AppDelegate. La función del delegado de la aplicación es responder a los métodos del ciclo de vida de la aplicación y configurarla en consecuencia. Uno de estos métodos es aplicación (_: didFinishLaunchingWithOptions :), que se llama tan pronto como se inicia la aplicación. Ahí es donde creamos la instancia del Controlador de modelo y pasarlo a la QuoteViewController:

clase AppDelegate: UIResponder, UIApplicationDelegate var window: UIWindow? aplicación func (_ aplicación: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool if let quoteViewController = window? .rootViewController as? QuoteViewController quoteViewController.modelController = ModelController () return true

Nuestra aplicación ahora está completa. Cada controlador de vista obtiene acceso al estado global de la aplicación, pero no usamos singletons en ningún lugar de nuestro código.

Puede descargar el proyecto Xcode para esta aplicación de ejemplo en el tutorial GitHub repo.

Conclusiones

En este artículo, ha visto cómo utilizar singletons para propagar el estado en una aplicación iOS es una mala práctica. Los Singletons crean muchos problemas, a pesar de ser muy fáciles de crear y usar..

Resolvimos el problema observando más de cerca el patrón MVC y entendiendo las posibilidades ocultas en él. Mediante el uso de controladores de modelo y la inyección de dependencia, pudimos propagar el estado de la aplicación en todos los controladores de vista sin utilizar singletons.

Esta es una aplicación de ejemplo simple, pero el concepto puede generalizarse a aplicaciones de cualquier complejidad. Esta es la mejor práctica estándar para propagar el estado en aplicaciones iOS. Ahora lo uso en todas las aplicaciones que escribo para mis clientes..

Algunas cosas a tener en cuenta al expandir el concepto a aplicaciones más grandes:

  • El controlador modelo puede guardar el estado de la aplicación, por ejemplo, en un archivo. De esta manera, nuestros datos serán recordados cada vez que cerramos la aplicación. También puede utilizar una solución de almacenamiento más compleja, por ejemplo, Core Data. Mi recomendación es mantener esta funcionalidad en un controlador modelo separado que solo se encargue del almacenamiento. Ese controlador puede ser utilizado por el controlador modelo que mantiene el estado de la aplicación.
  • En una aplicación con un flujo más complejo, tendrá muchos contenedores en su flujo de aplicaciones. Estos suelen ser los controladores de navegación, con el controlador de barra de pestañas ocasional. El concepto de inyección de dependencia todavía se aplica, pero debe tener en cuenta los contenedores. Puede excavar en sus controladores de vista contenida al realizar la inyección de dependencia, o crear subclases de contenedores personalizados que pasan el controlador de modelo..
  • Si agrega redes a su aplicación, esto también debe ir en un controlador modelo separado. Un controlador de vista puede realizar una solicitud de red a través de este controlador de red y luego pasar los datos resultantes al controlador modelo que mantiene el estado. Recuerde que el rol de un controlador de vista es exactamente este: actuar como un objeto de pegamento que pasa datos entre objetos.

Manténgase sintonizado para obtener más consejos de desarrollo de aplicaciones iOS y mejores prácticas!