Adaptando su solución comercial existente para SwiftUI. Parte 3. Trabajando con arquitectura

¡Buen día a todos! Contigo, yo, Anna Zharkova, un desarrollador móvil líder de Usetech,

seguimos desmontando las complejidades de SwiftUI. Las partes anteriores se pueden encontrar en los enlaces:

parte 1
parte 2

Hoy hablaremos sobre las características de la arquitectura y cómo transferir e integrar la lógica comercial existente en la aplicación SwiftUI.

El flujo de datos estándar en SwiftUI se basa en la interacción de View y un cierto modelo que contiene propiedades y variables de estado, o que es una variable de estado. Por lo tanto, es lógico que MVVM sea el socio arquitectónico recomendado para las aplicaciones SwiftUI. Apple sugiere usarlo junto con el marco Combine, que presenta Api SwiftUI declarativa para procesar valores a lo largo del tiempo. ViewModel implementa el protocolo ObservableObject y se conecta como un Objeto Observado a una Vista específica.



Las propiedades del modelo modificables se declaran como @Published.

class NewsItemModel: ObservableObject {
    @Published var title: String = ""
    @Published var description: String = ""
    @Published var image: String = ""
    @Published var dateFormatted: String = ""
}

Al igual que en el MVVM clásico, ViewModel se comunica con un modelo de datos (es decir, lógica de negocios) y transfiere datos de una forma u otra Vista.

struct NewsItemContentView: View {
    @ObservedObject var moder: NewsItemModel
    
    init(model: NewsItemModel) {
        self.model = model 
    }
    //... - 
}

MVVM, como casi cualquier otro patrón, tiene tendencia a la congestión y la
redundancia. ViewModel sobrecargado siempre depende de qué tan bien se resalte y abstraiga la lógica de negocios. La carga de la Vista está determinada por la complejidad de la dependencia de los elementos de las variables de estado y las transiciones a otra Vista.

En SwiftUI, lo que se agrega es que View es una estructura, no una clase , y por lo tanto no admite la herencia, lo que obliga a la duplicación de código.
Si en aplicaciones pequeñas esto no es crítico, con el crecimiento de la funcionalidad y la complejidad de la lógica, la sobrecarga se vuelve crítica y una gran cantidad de copiar y pegar inhibe.

Intentemos utilizar el enfoque de código limpio y arquitectura limpia en este caso. No podemos abandonar MVVM por completo, después de todo, DataFlow SwiftUI está construido sobre él, pero es bastante reconstruible.

¡Advertencia!

Si eres alérgico a los artículos sobre arquitectura, y el código limpio se vuelve del revés de una frase, desplázate un par de párrafos hacia abajo.
¡Este no es exactamente el código limpio del tío Bob!


Sí, no tomaremos el código limpio del tío Bob en su forma más pura. En cuanto a mí, hay un exceso de ingeniería. Tomaremos solo una idea.

La idea principal del código limpio es crear el código más legible, que luego se puede ampliar y modificar sin problemas.

Hay bastantes principios de desarrollo de software que se recomienda cumplir.



Muchos los conocen, pero no todos los aman y no todos los usan. Este es un tema separado para holivar.

Para garantizar la pureza del código, al menos es necesario dividir el código en capas y módulos funcionales, usar la solución general de problemas e implementar la abstracción de la interacción entre los componentes. Y al menos debe separar el código de la interfaz de usuario de la llamada lógica empresarial.

Independientemente del patrón arquitectónico seleccionado, la lógica de trabajar con la base de datos y la red, el procesamiento y el almacenamiento de datos se separa de la interfaz de usuario y los módulos de la propia aplicación. Al mismo tiempo, los módulos funcionan con implementaciones de servicios o almacenamientos, que a su vez acceden al servicio general de solicitudes de red o al almacenamiento general de datos. La inicialización de variables mediante las cuales puede acceder a uno u otro servicio se realiza en un determinado contenedor general, al que finalmente accede el módulo de aplicación (lógica de negocios del módulo).



Si hemos seleccionado y abstraído la lógica de negocios, entonces podemos organizar la interacción entre los componentes de los módulos a nuestro gusto.

En principio, todos los patrones existentes de las aplicaciones de iOS funcionan con el mismo principio.



Siempre hay lógica de negocios, hay datos. También hay un administrador de llamadas, que es responsable de la presentación y transformación de los datos para la salida y de dónde salen los datos convertidos. La única diferencia es cómo se distribuyen los roles entre los componentes.


Porque Nos esforzamos para que la aplicación sea legible, para simplificar los cambios actuales y futuros, es lógico separar todos estos roles. Nuestra lógica de negocios ya ha sido resaltada, los datos siempre están separados. Permanezca despachador, presentador y vista. Como resultado, obtenemos una arquitectura que consiste en View-Interactor-Presenter, en el que el interactor interactúa con los servicios de lógica de negocios, el presentador convierte los datos y los entrega como una especie de ViewModel a nuestra Vista. En el buen sentido, la navegación y la configuración también se eliminan de la vista en componentes separados.



Obtenemos la arquitectura VIP + R con la división de roles controvertidos en diferentes componentes.

Tratemos de ver un ejemplo. Tenemos una pequeña aplicación de agregación de noticias
escrita en SwiftUI y MVVM.



La aplicación tiene 3 pantallas separadas con su propia lógica, es decir, 3 módulos:


  • módulo de lista de noticias;
  • módulo de pantalla de noticias;
  • módulo de búsqueda de noticias.

Cada uno de los módulos consta de un ViewModel, que interactúa con la lógica de negocios seleccionada, y una Vista, que muestra lo que ViewModel le transmite.



Nos esforzamos por garantizar que ViewModel solo se preocupe por almacenar datos que estén listos para su visualización. Ahora se dedica tanto a acceder a los servicios como a procesar los resultados.

Transferimos estos roles al presentador y al interactor, que configuramos para cada
módulo.



El interactor pasa los datos recibidos del servicio al presentador, que llena el ViewModel existente vinculado a la Vista con datos preparados. En principio, con respecto a la separación de la lógica de negocios del módulo, todo es simple. 


Ahora ve a Ver. Intentemos lidiar con la duplicación forzada de código. Si estamos tratando con algún tipo de control, entonces pueden ser sus estilos o configuraciones. Si estamos hablando de la Vista en pantalla, entonces esto:

  • Estilos de pantalla
  • elementos comunes de la interfaz de usuario (LoadingView);
  • alertas de información;
  • Algunos métodos generales.

No podemos usar la herencia, pero sí podemos usar la composición . Es por este principio que se crean todas las Vistas personalizadas en SwiftUI.

Entonces, creamos un contenedor de Vista, en el cual transferimos la misma lógica, y pasamos nuestra Vista en pantalla al inicializador del contenedor y luego lo usamos como una Vista de contenido dentro del cuerpo.

struct ContainerView<Content>: IContainer, View where Content: View {
    @ObservedObject var containerModel = ContainerModel()
    private var content: Content

    public init(content: Content) {
        self.content = content
    }

    var body : some View {
        ZStack {
            content
            if (self.containerModel.isLoading) {
                LoaderView()
            }
        }.alert(isPresented: $containerModel.hasError){
            Alert(title: Text(""), message: Text(containerModel.errorText),
                 dismissButton: .default(Text("OK")){
                self.containerModel.errorShown()
                })
        }
    }

La vista en pantalla está incrustada en el ZStack dentro del contenedor ContainerView, que también contiene código para mostrar LoadingView y código para mostrar una alerta de información.

También necesitamos nuestro ContainerView para recibir una señal del ViewModel de la Vista interna y actualizar su estado. No podemos suscribirnos a través de @Observed al mismo modelo
que la Vista interna, porque arrastraremos sus señales.



Por lo tanto, establecemos comunicación con él a través del patrón de delegado, y para el estado actual del contenedor usamos su propio ContainerModel.

class ContainerModel:ObservableObject {
    @Published var hasError: Bool = false
    @Published var errorText: String = ""
    @Published var isLoading: Bool = false
    
    func setupError(error: String){
     //....
       }
    
    func errorShown() {
     //...
    }
    
    func showLoading() {
        self.isLoading = true
    }
    
    func hideLoading() {
        self.isLoading = false
    }
}

ContainerView implementa el protocolo IContainer; se asigna una referencia de instancia al modelo de Vista incrustado.

protocol  IContainer {
    func showError(error: String)
    
    func showLoading()
    
    func hideLoading()
}

struct ContainerView<Content>: IContainer, View where Content: View&IModelView {
    @ObservedObject var containerModel = ContainerModel()
    private var content: Content

    public init(content: Content) {
        self.content = content
        self.content.viewModel?.listener = self
    }
    //- 
}

View implementa el protocolo IModelView para encapsular el acceso al modelo y unificar cierta lógica. Los modelos con el mismo propósito implementan el protocolo IModel:

protocol IModelView {
    var viewModel: IModel? {get}
}

protocol  IModel:class {
   //....
    var listener:IContainer? {get set}
}

Entonces, ya en este modelo, si es necesario, se llama al método delegado, por ejemplo, para mostrar una alerta con un error en el que cambia la variable de estado del modelo de contenedor.

struct ContainerView<Content>: IContainer, View where Content: View&IModelView {
    @ObservedObject var containerModel = ContainerModel()
    private var content: Content

    //- 
    func showError(error: String) {
        self.containerModel.setupError(error: error)
    }
    
    func showLoading() {
        self.containerModel.showLoading()
    }
    
    func hideLoading() {
        self.containerModel.hideLoading()
    }
}

Ahora podemos unificar el trabajo de la Vista cambiando al trabajo a través de ContainerView.
Esto facilitará enormemente nuestras vidas cuando trabajemos con la configuración de los siguientes módulos y navegación.
Cómo configurar la navegación en SwiftUI y hacer una configuración limpia, hablaremos en la siguiente parte .

Puede encontrar el código fuente del ejemplo aquí .

All Articles