Ponga sus controladores de vista en una dieta con MVVM

En mi publicación anterior en esta serie, escribí sobre el patrón Modelo-Vista-Controlador y algunas de sus imperfecciones. A pesar de los claros beneficios que MVC aporta al desarrollo de software, tiende a fallar en aplicaciones Cocoa grandes o complejas..

Aunque esto no es noticia. Varios patrones arquitectónicos han surgido a lo largo de los años, con el objetivo de abordar las deficiencias del patrón Modelo-Vista-Controlador. Usted puede haber oído hablar de MVP, Model-View-Presenter, y MVVM, Model-View-ViewModel, por ejemplo. Estos patrones se ven y se sienten similares al patrón Modelo-Vista-Controlador, pero también abordan algunos de los problemas que el patrón Modelo-Vista-Controlador sufre.

1. ¿Por qué Model-View-ViewModel

Había estado usando el patrón Modelo-Vista-Controlador durante años antes de que accidentalmente tropecé con el Model-View-ViewModel modelo. No es de extrañar que MVVM llegue tarde a la comunidad Cocoa, ya que sus orígenes se remontan a Microsoft. Sin embargo, el patrón MVVM se ha adaptado a Cocoa y se ha adaptado a los requisitos y necesidades de los marcos de Cocoa, y recientemente ha estado ganando terreno en la comunidad de Cocoa..

Lo más atractivo es cómo MVVM se siente como una versión mejorada del patrón Modelo-Vista-Controlador. Esto significa que no requiere un cambio dramático de mentalidad. De hecho, una vez que entiendes los fundamentos del patrón, es bastante fácil de implementar, no más difícil que implementar el patrón Modelo-Vista-Controlador.

2. Poner controladores de vista en una dieta

En la publicación anterior, escribí que los controladores en una aplicación Cocoa típica son un poco diferentes de los controladores Reenskaug definidos en el patrón MVC original. En iOS, por ejemplo, un controlador de vista controla una vista. Su única responsabilidad es poblar la vista que gestiona y responder a la interacción del usuario. Pero esa no es la única responsabilidad de los controladores de vista en la mayoría de las aplicaciones de iOS.?

El patrón MVVM introduce un cuarto componente a la mezcla, el ver modelo, lo que ayuda a reenfocar el controlador de vista. Lo hace asumiendo algunas de las responsabilidades del controlador de vista. Eche un vistazo al diagrama a continuación para comprender mejor cómo el modelo de vista se ajusta al patrón Modelo-Vista-Modelo de Vista..

Como se ilustra en el diagrama, el controlador de vista ya no posee el modelo. Es el modelo de vista que posee el modelo, y el controlador de vista solicita al modelo de vista los datos que necesita mostrar.

Esta es una diferencia importante con respecto al patrón Modelo-Vista-Controlador. El controlador de vista no tiene acceso directo al modelo. El modelo de vista entrega al controlador de vista los datos que necesita mostrar en su vista.

La relación entre el controlador de vista y su vista permanece sin cambios. Eso es importante porque significa que el controlador de vista puede centrarse exclusivamente en poblar su vista y manejar la interacción del usuario. Para eso fue diseñado el controlador de vista.

El resultado es bastante dramático. El controlador de vista se pone a dieta, y muchas responsabilidades se desplazan al modelo de vista. Ya no terminas con un controlador de vista que abarca cientos o incluso miles de líneas de código.

3. Responsabilidades del modelo de vista

Probablemente te estés preguntando cómo encaja el modelo de vista en la imagen más grande. ¿Cuáles son las tareas del modelo de vista? ¿Cómo se relaciona con el controlador de vista? ¿Y qué pasa con el modelo??

El diagrama que te mostré anteriormente nos da algunos consejos. Vamos a empezar con el modelo. El modelo ya no es propiedad del controlador de vista. El modelo de vista es propietario del modelo y actúa como un proxy para el controlador de vista. Cada vez que el controlador de vista necesita una parte de los datos de su modelo de vista, este último solicita a su modelo los datos en bruto y los formatea de tal manera que el controlador de vista puede usarlos inmediatamente en su vista. El controlador de vista no es responsable de la manipulación y el formato de los datos..

El diagrama también revela que el modelo es propiedad del modelo de vista, no del controlador de vista. También vale la pena señalar que el patrón Model-View-ViewModel respeta la estrecha relación entre el controlador de vista y su vista, que es característica de las aplicaciones Cocoa. Es por eso que MVVM se siente como un ajuste natural para las aplicaciones de Cocoa..

4. Un ejemplo

Debido a que el patrón Model-View-ViewModel no es nativo de Cocoa, no hay reglas estrictas para implementar el patrón. Desafortunadamente, esto es algo que muchos desarrolladores confunden. Para aclarar algunas cosas, me gustaría mostrarle un ejemplo básico de una aplicación que utiliza el patrón MVVM. Creamos una aplicación muy simple que obtiene datos del clima para una ubicación predefinida desde la API de Dark Sky y muestra la temperatura actual al usuario.

Paso 1: Configura el proyecto

Arranque Xcode y cree un nuevo proyecto basado en el Solicitud de vista única modelo. Estoy usando Xcode 8 y Swift 3 para este tutorial.

Nombra el proyecto MVVM, y establecer Idioma a Rápido y Dispositivos a iPhone.

Paso 2: crear un modelo de vista

En una aplicación Cocoa típica impulsada por el patrón Modelo-Vista-Controlador, el controlador de la vista se encargaría de realizar la solicitud de red. Podría usar un administrador para realizar la solicitud de red, pero el controlador de vista aún sabría acerca de los orígenes de los datos meteorológicos. Más importante aún, recibiría los datos en bruto y tendría que formatearlos antes de mostrarlos al usuario. Este no es el enfoque que adoptamos al adoptar el modelo Model-View-ViewModel.

Vamos a crear un modelo de vista. Crea un nuevo archivo Swift, llámalo WeatherViewViewModel.swift, y definir una clase llamada WeatherViewViewModel.

importar clase de Fundación WeatherViewViewModel 

La idea es simple. El controlador de vista solicita al modelo de vista la temperatura actual para una ubicación predefinida. Debido a que el modelo de vista envía una solicitud de red a la API de Dark Sky, el método acepta un cierre, que se invoca cuando el modelo de vista tiene datos para el controlador de vista. Esos datos podrían ser la temperatura actual, pero también podría ser un mensaje de error. Esto es lo que el temperatura actual (finalizacion :) El método del modelo de vista parece. Completaremos los detalles en unos instantes..

importar clase de base WeatherViewViewModel // MARK: - Tipografías de alias de tipo CurrentTemperatureCompletion = (String) -> Void // MARK: - Función de la API pública currentTemperature (completado: @escaping CurrentTemperatureCompletion) 

Declaramos un alias de tipo para mayor comodidad y definimos un método., temperatura actual (finalizacion :), Que acepte un cierre de tipo. CurrentTemperatureCompletion

La implementación no es difícil si está familiarizado con la red y la URLSession API. Eche un vistazo al código a continuación y observe que he usado una enumeración., API, para mantener todo agradable y ordenado.

importar clase Foundation WeatherViewViewModel // MARK: - Escriba alias typealias CurrentTemperatureCompletion = (String) -> Void // MARK: - API enumeración API static let lat = 37.8267 static let long = -122.4233 static let APIKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" static let baseURL = URL (cadena: "https://api.darksky.net/forecast")! static var requestURL: URL return API.baseURL .appendingPathComponent (API.APIKey) .appendingPathComponent ("\ (lat), \ (long)") // MARK: - Public API func currentTemperature (complete: @escaping CurrentTemperatureCompletion) let dataTask = URLSession.shared.dataTask (con: API.requestURL) [self débil] (datos, respuesta, error) en // Helpers var formattedTemperature: String? if let data = data formattedTemperature = self? .temperature (from: data) DispatchQueue.main.async completed (formattedTemperature ?? "No se pueden obtener datos del clima") // Reanudar datos Tarea dataTask.resume () 

El único código que no te he mostrado aún es la implementación del temperatura (desde :) método. En este método, extraemos la temperatura actual de la respuesta del cielo oscuro.

// MARK: - Temperatura de la función de los métodos de ayuda (de datos: Datos) -> ¿Cadena? Guardia deja JSON = tratar? JSONSerialization.jsonObject (con: datos, opciones: []) como? [Cadena: Cualquier] else return nil guard let current = JSON? ["Current"] as? [Cadena: Cualquiera] else return nil guarda dejar temperatura = actualmente ["temperatura"] como? Double else return nil return String (formato: "% .0f ° F", temperatura)

En una aplicación de producción, optaría por una solución más robusta para analizar la respuesta, como ObjectMapper o Unbox..

Paso 3: Integrar el modelo de vista

Ahora podemos usar el modelo de vista en el controlador de vista. Creamos una propiedad para el modelo de vista y también definimos tres salidas para la interfaz de usuario.

importar clase UIKit ViewController: UIViewController // MARK: - Propiedades @IBOutlet var temperatureLabel: UILabel! // MARK: - @IBOutlet var fetchWeatherDataButton: UIButton! // MARK: - @IBOutlet var activityIndicatorView: UIActivityIndicatorView! // MARK: - privado deja viewModel = WeatherViewViewModel ()

Observe que el controlador de vista posee el modelo de vista. En este ejemplo, el controlador de vista también es responsable de crear una instancia de su modelo de vista. En general, prefiero inyectar el modelo de vista en el controlador de vista, pero seamos sencillos por ahora..

En el controlador de vista viewDidLoad () Método, invocamos un método auxiliar., fetchWeatherData ().

// MARCAR: - Ver función de anulación del ciclo de vida viewDidLoad () super.viewDidLoad () // Fetch Weather Data fetchWeatherData ()

En fetchWeatherData (), Le pedimos al modelo de vista la temperatura actual. Antes de solicitar la temperatura, ocultaremos la etiqueta y el botón y mostraremos la vista del indicador de actividad. En el cierre pasamos a fetchWeatherData (finalización :), Actualizamos la interfaz de usuario rellenando la etiqueta de temperatura y ocultando la vista del indicador de actividad.

// MARCAR: - Métodos de ayuda private func fetchWeatherData () // Ocultar la interfaz de usuario temperatureLabel.isHidden = true fetchWeatherDataButton.isHidden = true // Mostrar indicador de actividad View activityIndicatorView.startAnimating () // Fetch Weather Data viewModel.currentTemperature [unowned self] (temperatura) en // Actualizar la etiqueta de temperatura self.temperatureLabel.text = temperature self.temperatureLabel.isHidden = false // Mostrar el botón de Fetch Weather Data self.fetchWeatherDataButton.isHidden = false // Ocultar indicador de actividad View self.activityIndicatorView.stopAnimating ()

El botón está conectado a una acción., fetchWeatherData (_ :), en el que también invocamos el fetchWeatherData () método de ayuda Como puede ver, el método auxiliar nos ayuda a evitar la duplicación de código.

// MARK: - Actions @IBAction func fetchWeatherData (_ sender: Any) // Fetch Weather Data fetchWeatherData ()

Paso 4: Crear la interfaz de usuario

La última pieza del rompecabezas es crear la interfaz de usuario de la aplicación de ejemplo. Abierto Main.storyboard y agregue una etiqueta y un botón a una vista de pila vertical. También agregaremos una vista de indicador de actividad en la parte superior de la vista de pila, centrada vertical y horizontalmente.

No olvide conectar los puntos de venta y la acción que definimos en el ViewController clase!

Ahora construye y ejecuta la aplicación para darle una oportunidad. Recuerde que necesita una clave de API de Dark Sky para que la aplicación funcione. Puedes registrarte para obtener una cuenta gratuita en el sitio web de Dark Sky.

5. ¿Cuáles son los beneficios?

Aunque solo movimos algunos bits y piezas al modelo de vista, puede que se esté preguntando por qué esto es necesario. ¿Qué ganamos? ¿Por qué agregarías esta capa adicional de complejidad??

La ganancia más obvia es que el controlador de vista es más ágil y más enfocado en administrar su vista. Esa es la tarea central de un controlador de vista: administrar su vista.

Pero hay un beneficio más sutil. Debido a que el controlador de vista no es responsable de obtener los datos meteorológicos de la API de Dark Sky, no tiene conocimiento de los detalles relacionados con esta tarea. Los datos meteorológicos podrían provenir de un servicio meteorológico diferente o de una respuesta en caché. El controlador de vista no lo sabría, y no necesita saberlo..

La prueba también mejora dramáticamente. Se sabe que los controladores de vista son difíciles de probar debido a su estrecha relación con la capa de vista. Al trasladar parte de la lógica empresarial al modelo de vista, mejoramos instantáneamente la capacidad de prueba del proyecto. Probar modelos de vista es sorprendentemente fácil porque no tienen un enlace a la capa de vista de la aplicación.

Conclusión

El patrón Model-View-ViewModel es un importante paso adelante en el diseño de aplicaciones Cocoa. Los controladores de vista no son tan masivos, los modelos de vista son más fáciles de componer y probar, y su proyecto se vuelve más manejable como resultado.

En esta corta serie, solo arañamos la superficie. Hay mucho más que escribir sobre el patrón Model-View-ViewModel. Se ha convertido en uno de mis patrones favoritos a lo largo de los años, y es por eso que sigo hablando y escribiendo sobre ello. Pruébalo y déjame saber lo que piensas.!

Mientras tanto, echa un vistazo a algunas de nuestras otras publicaciones sobre el desarrollo de aplicaciones Swift y iOS.