MVVM basados ​​en combinación en aplicaciones UIKit y SwiftUI para desarrolladores de UIKit



Sabemos que las ObservableObjecclases t con sus @Publishedpropiedades se crean Combineespecialmente para View Modelin SwiftUI. Pero exactamente lo mismo View Modelse puede utilizar en la UIKitimplementación de la arquitectura  MVVM, aunque en este caso tendremos que “vincular” ( bind) manualmente los UIelementos a las @Published propiedades de View Model. Te sorprenderás, pero con la ayuda de Combineesto puedes hacer un par de líneas de código. Además, al adherirse a esta ideología al diseñar UIKitaplicaciones, posteriormente cambiará sin problemas SwiftUI.

El propósito de este artículo es mostrar con un ejemplo primitivamente simple cómo implementar una MVVMarquitectura UIKitcon elegancia  Combine. Por el contrario, mostramos el uso del mismoView Model c SwiftUI.

El artículo discutirá dos aplicaciones simples que le permiten seleccionar la información meteorológica más reciente para una ciudad específica del sitio OpenWeatherMa p. Pero UIuno de ellos se creará con la aplicación SwiftUIy el otro con ayuda UIKit. Para el usuario, estas aplicaciones se verán casi iguales.



El código está en Github .

La interfaz de usuario ( UI) contendrá solo 2 UI elementos: un campo de texto para ingresar la ciudad y una etiqueta para mostrar la temperatura. El cuadro de texto para ingresar a la ciudad es la ENTRADA activa ( Input), y la etiqueta de temperatura es la SALIDA pasiva ( Output).  

El papel View Model en la arquitectura MVVMes que toma las ENTRADAS de View(o ViewControlleren UIKit), implementa la lógica de negocios de la aplicación y devuelve las SALIDAS a  View(o ViewControlleren UIKit), posiblemente presentando estos datos en el formato deseado.

Creación  View Modelcon Combineno importa qué tipo de lógica de negocio - sincrónica o asincrónica - es muy simple si se utiliza una ObservableObject clase con sus @Publishedpropiedades.

API OpenWeatherMap


Aunque el servicio  OpenWeatherMap le   permite seleccionar información meteorológica muy extensa, el modelo de los datos que nos interesa será muy simple, proporciona información detallada  WeatherDetailsobre el clima actual en la ciudad seleccionada y se encuentra en el archivo  Model.swift :



aunque en esta tarea específica solo nos interesará la temperatura temp, que está en la estructura  Main, el Modelo proporciona información detallada completa sobre el clima actual como estructura raíz  WeatherDetail, creyendo que en el futuro querrá ampliar las capacidades de esta aplicación. La estructura WeatherDetail es codificable, esto nos permitirá decodificar literalmente los JSONdatos en el modelo con solo dos líneas de código  .

La estructura  WeatherDetail también debe serIdentifiablesi queremos que sea más fácil para nosotros en el futuro mostrar una serie de pronósticos meteorológicos  [WeatherDetail] con varios días de anticipación en forma de una lista  List de SwiftUI. Esto también es un espacio en blanco para una futura aplicación meteorológica actual más sofisticada. El protocolo Identifiablerequiere la presencia de la propiedad id,que ya tenemos, por lo que no se requerirán esfuerzos adicionales de nuestra parte.

Por lo general, los servicios, incluido el servicio  OpenWeatherMap , ofrecen todo tipo de servicios URLs para obtener los recursos que necesitamos. El servicio  OpenWeatherMap nos ofrece URLsobtener información detallada sobre el clima actual o el pronóstico para 5 días en una determinada ciudad city. En esta aplicación, solo nos interesará la información meteorológica actual y para este casoURLcalculado utilizando la función absoluteURL (city: String):



API para el servicio OpenWeatherMap ,  lo colocaremos en el archivo WeatherAPI.swift . Su parte central será un método para seleccionar información meteorológica detallada  WeatherDetailen una ciudad  city:

  • fetchWeather (for city: String) -> AnyPublisher<WeatherDetail, Never>

En el contexto del marco, Combine este método devuelve no solo información meteorológica detallada  WeatherDetail, sino el "editor" correspondiente Publisher. Nuestro "editor" AnyPublisher<WeatherDetail, Never>no devuelve ningún error, Nevery si aún se produce un error de muestreo o codificación, el suplente regresa  WeatherDetail.placeholdersin ningún mensaje adicional sobre la causa del error. 

Considere con más detalle el método  fetchWeather (for city: String) -> AnyPublisher<WeatherDetail, Never>que selecciona  información meteorológica detallada para la ciudad del sitio web OpenWeatherMap city y no devuelve ningún error Never:



  1. según el nombre de la ciudad,  city formamos URL utilizando la función absoluteURL(city:city)para solicitar información meteorológica detallada  WeatherDetail,
  2. «» dataTaskPublisher(for:), Output (data: Data, response: URLResponse),   Failure - URLError,
  3. map { } (data: Data, response: URLResponse)  data
  4. JSON  data ,  WeatherDetail, ,
  5. - «»  catch (error ... )  «» WeatherDetail.placeholder,
  6. main , UI,
  7. «» «» eraseToAnyPublisher() AnyPublisher.

El "editor" asíncrono así obtenido AnyPublisher"no despega" por sí mismo, no entrega nada hasta que alguien "se suscribe" a él. Lo usaremos en una  ObservableObject clase que juega un papel View Modelen  SwiftUIambos y UIKit

Crear modelo de vista


Para View Modelcrear una clase muy simple  TempViewModelque implemente un protocolo ObservableObject con dos  @Published propiedades:  



  1. uno  @Published var city: Stringes la ciudad (puede llamarlo condicionalmente una ENTRADA, ya que su valor está regulado por el usuario en View),  
  2. el segundo  @Published var currentWeather = WeatherDetail.placeholder es el clima en esta ciudad en este momento (podemos llamar condicionalmente a esta propiedad EXIT, ya que se obtiene al obtener datos del sitio web de  OpenWeatherMap ).

Una vez que hemos establecido una  @Published propiedad  city, podemos comenzar a usarla como una propiedad simple  cityy como un "editor"  $city.

En una clase  TempViewModel, no solo puede declarar las propiedades que nos interesan, sino también prescribir la lógica comercial de su interacción. Con este fin, al inicializar una instancia de la clase  TempViewModel en, init?podemos crear una "suscripción" que operará durante todo el "ciclo de vida" de la instancia de la clase  TempViewModely reproducirá la dependencia del clima actual  currentWeather en la ciudad  city.

Para hacer esto, Combineestiramos la cadena desde el "editor" de entrada hasta el "editor" $city de salida AnyPublisher<WeatherDetail, Never>, cuyo valor es el clima actual. Posteriormente, nos "suscribimos" con la ayuda de un "suscriptor" assign (to: \.currentWeather, on: self) y obtenga el valor deseado del clima actual  currentWeather como una @Published propiedad de "salida"  .

Debemos extraer la cadena NO simplemente de las propiedades city, es decir, de los "editores" $cityque participarán en la creación UI y es allí donde la cambiaremos.

¿Como haremos esto?

Ya tenemos una función en nuestro arsenal fetchWeather (for city: String)que está en la clase  WeatherAPIy devuelve al "editor" AnyPublisher<WeatherDetail, Never> con información meteorológica detallada según la ciudad  city, y de alguna manera solo podemos usar el valor del "editor"  $citypara convertirlo en un argumento de esta función.

 Diríjase al editor correcto  fetchWeather (for city: String) para  Combineayudarnos operador  flatMap:



OperadorflatMapcrea un nuevo "editor" basado en los datos recibidos del "editor" anterior.

A continuación, nos "suscribimos" a este "editor" recién recibido con la ayuda de un "suscriptor" muy simple  assign (to: \.currentWeather, on: self)y asignamos el valor recibido del "editor" a la @Publishedpropiedad  currentWeather:



acabamos de crear un init( )"editor" ASINCRÓNICO y "suscrito" a él, lo que resulta en una AnyCancellable"suscripción" ".

AnyCancellable La "suscripción" permite que la persona que llama cancele la "suscripción" en cualquier momento y ya no reciba valores del "editor", pero además, tan pronto como la  AnyCancellable"suscripción" abandone su alcance, se libera la memoria ocupada por el "editor". Por lo tanto, tan pronto como se init( ) complete, esta "suscripción" será eliminada por el sistema ARC, y no tener tiempo para asignar la información asincrónica sobre el clima actual recibido con un retraso de tiempo  currentWeather. Para guardar dicha "suscripción", es necesario crear una init()variable EXTERIOR var cancellableSetque mantendrá nuestra AnyCancellable"suscripción" en esta variable durante todo el "ciclo de vida" de la instancia de clase  TempViewMode

La AnyCancellable"suscripción" en la variable se almacena cancellableSetutilizando el operador  store ( in: &self.cancellableSet):



Como resultado, la "suscripción" se conservará durante todo el "ciclo de vida" de la instancia de clase  TempViewModel. Podemos cambiar el valor del editor según lo desee $city, y el clima actual currentWeather de esta ciudad siempre estará a nuestra disposición .

Para reducir la cantidad de llamadas al servidor al escribir una ciudad city, no deberíamos usar directamente el "editor" de la línea con el nombre de la ciudad  $city, sino su versión modificada con operadores debouncey removeDuplicates:



El operador se  debounce usa para esperar hasta que el usuario termine de escribir la información necesaria en el teclado, y solo luego realice la tarea de uso intensivo de recursos una vez.

Del mismo modo, un operador removeDuplicatespublicará valores solo si difieren de los valores anteriores. Por ejemplo, si el usuario ingresa primero john, luego joe, y luego nuevamente john, recibiremos johnsolo una vez. Esto ayuda a que el nuestro sea UImás eficiente.

Crear una interfaz de usuario con SwiftUI


Ahora que lo tenemos View Model, comencemos UI. Primero a las SwiftUI, y luego a las UIKit.

En Xcodecreamos un nuevo proyecto con SwiftUIy en la estructura resultante colocamos el ContentView  nuestro  View Modelcomo @ObservedObject variable model. Reemplace  Text ("Hello, World!") con el título  Text ("WeatherApp"), agregue un cuadro de texto para ingresar la ciudad  TextField ("City", text: self.$model.city) y una etiqueta para mostrar la temperatura:



Utilizamos directamente los valores de nuestra variable model: TempViewModel(). Usamos en el cuadro de texto para ingresar la ciudad $model.city, y en la etiqueta para mostrar la temperatura - model.currentWeather.main?.temp.

Ahora, cualquier cambio en las  @Published propiedades conducirá a "redibujar" View:



esto está garantizado por el hecho de que el nuestro View Model es@ObservedObject, es decir, se realiza un "enlace" AUTOMÁTICO ( binding) @Publishedde nuestras propiedades View Modely elementos de interfaz de usuario ( UI). Tal "enlace" AUTOMÁTICO solo es posible en SwiftUI.

Crear una interfaz de usuario con UIKit


¿Qué hacer con esto en UIKit? No esta ahi  @ObservedObject. En  UIKit realizaremos el "enlace" ( binding) manualmente. Hay muchas formas de hacer este "enlace manual":

  • Key-Value Observing o KVO: un mecanismo para usar  key pathspara monitorear una propiedad y recibir notificación de que ha cambiado.
  • Programación funcional reactiva o FRP: uso de un framework Combine.
  • Delegation: Uso de métodos delegados para enviar una notificación de que el valor de una propiedad ha cambiado.
  • Boxing: didSet { } , .

Dado el título del artículo, naturalmente trabajaremos en el campo Combine. En la UIKitaplicación, mostraremos lo fácil que es hacer un "enlace manual" Combine.

En la UIKitaplicación, también tendremos dos UI elementos: UITextFieldpara ingresar a la ciudad y UILabelpara mostrar la temperatura. En ViewControllernaturalmente a tener Outletestos elementos:





en la forma de una variable ordinaria viewModel, tenemos el mismo que View Modelen el apartado anterior:



Antes de hacer el “Manual de la unión” con Combine, vamos a hacer el campo de texto UITextFielda nuestro aliado y el “editor” de nuestro contenido text:



Esto nos permitirá viewDidLoadimplementar muy fácilmente el "enlace manual" usando la funciónbinding ():



En efecto, “suscribirse” a la “editorial” cityTextField.textPublishercon la ayuda de una manera muy sencilla “suscriptor”  assign (to: \.city, on: viewModel)y asignar el texto escrito por el usuario en el campo de texto cityTextFielda nuestra “entrada”  @Publishedde propiedad de citynuestra View Model.

Además, hacemos cambios en otra dirección: nos "suscribimos" a la @Publishedpropiedad  "salida"  $currentWeather con la ayuda del "suscriptor" sink y su cierre receiveValue, formamos el valor de temperatura y lo asignamos a la etiqueta temperatureLabel.

Recibido en la  viewDidLoad "suscripción" se almacena en una variable var cancellableSet. Habiéndolos creado una vez, les permitimos actuar durante todo el "ciclo de vida" de la instancia de clase ViewControllery junto con la "suscripción" en nuestro View ModelImplementar toda la lógica de negocios de la aplicación.

Por cierto, el protocolo ObservableObjectno funciona UIKit, pero no interfiere. UIKit completamente indiferente al protocolo ObservableObjecty, en principio, podría eliminarse  View Modelen las UIKit aplicaciones:



Pero no haremos esto, porque queremos mantenerlo sin cambios View Modeltanto para la aplicación actual UIKitcomo para las futuras aplicaciones SwiftUI.

Eso es todo. El código está en Github .

Conclusión

Un marco reactivo funcional le Combinepermite implementar de manera muy simple y concisa la MVVMarquitectura, tanto SwiftUIen UIKitforma de código comprensible como legible.

Enlaces:

Combinar + UIKit + MVVM
Uso de Combine
iOS MVVM Tutorial: Refactorización de MVC
MVVM con Combine Tutorial para iOS

PS Si desea ver información sobre el clima, debe registrarse en OpenWeatherMap  y obtener API key. Este proceso no te llevará más de 2 minutos.

All Articles