Interacci贸n de SwiftUI con Redux

imagen

Hola a todos. En este art铆culo hablaremos sobre el marco SwiftUI junto con Redux, este paquete nos permite crear aplicaciones de manera r谩pida y f谩cil. SwiftUI se utiliza para crear una interfaz de usuario de estilo declarativo, a diferencia de UIKit . Redux, a su vez, sirve para controlar el estado de la aplicaci贸n.

El estado es un concepto fundamental en SwiftUI y Redux. En nuestro caso, esto no es solo una palabra de moda, sino tambi茅n una entidad que los conecta y les permite trabajar muy bien juntos. En este art铆culo intentar茅 demostrar que la tesis anterior es cierta, 隆as铆 que comencemos!

Antes de sumergirnos en escribir c贸digo, primero comprendamos qu茅 es Redux y en qu茅 consiste.

Redux es una biblioteca de c贸digo abierto para administrar el estado de una aplicaci贸n. La mayor铆a de las veces se usa junto con React o Angular para desarrollar el lado del cliente. Contiene una serie de herramientas para simplificar significativamente la transferencia de datos de almacenamiento a trav茅s del contexto. Creadores: Daniil Abramov y Andrew Clark.

Para m铆, Redux no es solo una biblioteca, ya es algo m谩s, lo atribuyo a las decisiones arquitect贸nicas en las que se basa la aplicaci贸n. Principalmente debido a su flujo de datos unidireccional.

Flujo multidireccional o unidireccional.


Para explicar lo que quiero decir con flujo de datos, dar茅 el siguiente ejemplo. Una aplicaci贸n creada con VIPER admite un flujo de datos multidireccional entre m贸dulos:

imagen

Redux, a su vez, es un flujo de datos unidireccional y es m谩s f谩cil de explicar en funci贸n de sus componentes constituyentes.

imagen

Hablemos un poco en detalle sobre cada componente de Redux.

El estado es la 煤nica fuente de verdad que contiene toda la informaci贸n necesaria para nuestra aplicaci贸n.

La acci贸n es la intenci贸n de cambiar de estado. En nuestro caso, esta es una enumeraci贸n que contiene informaci贸n nueva que queremos agregar o cambiar en el estado actual.

ReductorEs una funci贸n que toma la acci贸n y el estado actual como par谩metros y devuelve un nuevo estado. Esta es la 煤nica forma de crearlo. Tambi茅n vale la pena se帽alar que esta caracter铆stica debe estar limpia.

Store es un objeto que contiene State y proporciona todas las herramientas necesarias para actualizarlo.

Creo que la teor铆a es suficiente, pasemos a la pr谩ctica.

Implementaci贸n de Redux


Una de las formas m谩s f谩ciles de conocer una herramienta es comenzar a usarla, como dijo mi maestro de programaci贸n, si desea aprender un lenguaje de programaci贸n, escriba una aplicaci贸n en 茅l. As铆 que creemos una peque帽a aplicaci贸n, que sea un simple diario de entrenamiento, tendr谩 solo cuatro opciones, la primera es mostrar una lista de entrenamientos, la segunda es agregar un entrenamiento completo, la tercera es eliminar y la cuarta es ordenar entrenamientos. Aplicaci贸n bastante simple, pero al mismo tiempo nos permitir谩 conocer Redux y SwiftUI.

Cree un proyecto limpio en Xcode, as铆gnele el nombre WorkoutsDiary y, lo m谩s importante, seleccione SwiftUI para la interfaz de usuario, ya que usaremos SwiftUI para crear nuestra interfaz de usuario.

Despu茅s de crear el proyecto. Cree una estructura de entrenamiento que ser谩 responsable del entrenamiento que completamos.

import Foundation

struct Workout: Identifiable {
    let id: UUID = .init()
    let name: String
    let distance: String
    let date: Date
    let complexity: Complexity
}

Como puede ver, esta estructura no tiene nada de loco, el campo de identificaci贸n se requiere para cumplir con el protocolo identificable, y el campo de complejidad es solo una enumeraci贸n con la siguiente definici贸n:

enum Complexity: Int {
    case low
    case medium
    case high
}

Ahora que tenemos todo lo que necesitamos para comenzar a implementar Redux, comencemos creando un Estado.

struct AppState {
    var workouts: [Workout]
    var sortType: SortType?
}

El estado es una estructura simple que contiene dos campos: entrenamientos y sortType . El primero es una lista de entrenamientos, y el segundo es un campo opcional que determina c贸mo se ordena la lista.

SortType es una enumeraci贸n que se define de la siguiente manera:

enum SortType {
    case distance
    case complexity
}

Para simplificar, clasificaremos por distancia y dificultad en orden descendente, es decir, cuanto mayor sea la complejidad de nuestro entrenamiento, mayor se mostrar谩 en nuestra lista. Vale la pena se帽alar que sortType es un tipo opcional y puede ser nulo, lo que significa que la lista no est谩 ordenada en este momento.

Continuaremos la implementaci贸n de nuestros componentes. Creemos una acci贸n

enum Action {
    case addWorkout(_ workout: Workout)
    case removeWorkout(at: IndexSet)
    case sort(by: SortType)
}

Como podemos ver, Acci贸n es una enumeraci贸n con tres casos que nos dan la capacidad de manipular a nuestro Estado .

  • addWorkout (_ workout: Workout) simplemente agrega un entrenamiento que se pasa como par谩metro.
  • removeWorkout (at: IndexSet) elimina el elemento en el 铆ndice especificado.
  • sort (by: SortType) ordena la lista de entrenamiento por el tipo de clasificaci贸n especificado.

Creemos uno de los componentes m谩s complejos, este es Reductor :

func reducer(state: AppState, action: Action) -> AppState {
    var state = state
    
    switch action {
    case .addWorkout(let workout):
        state.workouts.append(workout)
        
    case .removeWorkout(let indexSet):
        state.workouts.remove(atOffsets: indexSet)
    
    case .sort(let type):
        switch type {
        case .distance:
            state.workouts.sort { $0.distance > $1.distance }
            state.sortType = .distance
        case .complexity:
            state.workouts.sort { $0.complexity.rawValue > $1.complexity.rawValue }
            state.sortType = .complexity
        }
    }
    return state
}


La funci贸n que escribimos es bastante simple y funciona de la siguiente manera:

  1. Copia el estado actual para trabajar con 茅l.
  2. En funci贸n de Action , actualizamos nuestro estado copiado .
  3. Devuelve el estado actualizado

Vale la pena se帽alar que la funci贸n anterior es una funci贸n pura, 隆y eso es lo que quer铆amos lograr! Una funci贸n debe cumplir dos condiciones para ser considerada "pura":

  • Cada vez, la funci贸n devuelve el mismo resultado cuando se llama con el mismo conjunto de datos.
  • No hay efectos secundarios.

El 煤ltimo elemento que falta en Redux es la Tienda , as铆 que implem茅ntelo para nuestra aplicaci贸n.

final class Store: ObservableObject {
     @Published private(set) var state: AppState
     
     init(state: AppState = .init(workouts: [Workout]())) {
         self.state = state
     }
     
     public func dispatch(action: Action) {
         state = reducer(state: state, action: action)
     }
 }

En las implementaciones del objeto Store , utilizamos todas las ventajas del protocolo ObservableObject , que nos permite excluir la escritura de una gran cantidad de c贸digo de plantilla o el uso de marcos de terceros. La propiedad State es de solo lectura y utiliza el contenedor de la propiedad @Published , lo que significa que cada vez que se cambia, SwiftUI recibir谩 notificaciones. El m茅todo init toma un estado inicial como par谩metro con un valor predeterminado dado en forma de una matriz vac铆a de elementos de entrenamiento. La funci贸n de despacho es la 煤nica forma de actualizar el estado: reemplaza el estado actual con el nuevo creado por la funci贸n reductora , basado enAcci贸n , que se pasa como par谩metro.

Ahora que hemos implementado todos los componentes de Redux, podemos comenzar a crear una interfaz de usuario para nuestra aplicaci贸n.

Implementaci贸n de la aplicaci贸n


La interfaz de usuario de nuestra aplicaci贸n ser谩 bastante simple. Y constar谩 de dos pantallas peque帽as. La primera y principal pantalla es una pantalla que mostrar谩 una lista de entrenamientos. La segunda pantalla es la pantalla de agregar entrenamiento. Adem谩s, cada elemento se mostrar谩 en un color determinado, el color reflejar谩 la complejidad del entrenamiento. Los gl贸bulos rojos indican la mayor dificultad del entrenamiento, el naranja es responsable de la dificultad promedio y el verde es el responsable del entrenamiento m谩s f谩cil.

Implementaremos la interfaz usando un nuevo marco de Apple llamado SwiftUI. SwiftUI viene a reemplazar nuestro UIKit familiar. SwiftUI es fundamentalmente diferente de UIKit, principalmente porque es un enfoque declarativo para escribir elementos de UI con c贸digo. En este art铆culo, no profundizar茅 en todas las complejidades de SwiftUI y supongo que ya tiene experiencia con SwiftUI. Si no tiene conocimiento de SwiftUI, le aconsejo que preste atenci贸n a la documentaci贸n de Apple, es decir, mire sus varios tutoriales completos con la adici贸n paso a paso y la visualizaci贸n interactiva del resultado a la vista. Tambi茅n hay enlaces a proyectos de ejemplo. Estos tutoriales te permitir谩n sumergirte r谩pidamente en el mundo declarativo de SwiftUI.

Tambi茅n vale la pena se帽alar que SwiftUI a煤n no est谩 listo para proyectos de producci贸n, es demasiado joven y pasar谩 m谩s de un a帽o antes de que pueda usarse de esta manera. Adem谩s, no olvide que solo es compatible con las versiones iOS 13.0+. Pero tambi茅n vale la pena se帽alar que SwiftUI funcionar谩 en todas las plataformas de Apple, 隆lo cual es una gran ventaja sobre UIKit!

Comencemos la implementaci贸n desde la pantalla principal de nuestra aplicaci贸n. Vaya al archivo ContentView.swift y cambie el c贸digo actual a esto.

struct ContentView: View {
    @EnvironmentObject var store: Store
    @State private var isAddingMode: Bool = false
    
    var body: some View {
        NavigationView {
            WorkoutListView()
                .navigationBarTitle("Workouts diary", displayMode: .inline)
                .navigationBarItems(
                    leading: AddButton(isAddingMode: self.$isAddingMode),
                    trailing: TrailingView()
                )
        }
        .sheet(isPresented: $isAddingMode) {
            AddWorkoutView(isAddingMode: self.$isAddingMode)
                .environmentObject(self.store)
        }
    }
}

La Vista de contenido es una vista est谩ndar en SwiftUI. La parte m谩s importante, desde mi punto de vista, es la l铆nea de c贸digo que contiene la variable store. Crearemos @EnvironmentObject. Esto nos permitir谩 usar los datos de la Tienda donde sea necesario y, adem谩s, actualizar谩 autom谩ticamente nuestras vistas si se modifican los datos. Esto es algo as铆 como Singleton para nuestra tienda.

@EnvironmentObject var store: Store

Tambi茅n vale la pena se帽alar la siguiente l铆nea de c贸digo:

@State private var isAddingMode: Bool = false

EstadoEs un contenedor que podemos usar para indicar el estado de una Vista. SwiftUI lo almacenar谩 en una memoria interna especial fuera de la estructura de Vista. Solo una vista vinculada puede acceder a ella. Una vez que el valor de la propiedadEstado cambios, SwiftUI reconstruye la Vista para tener en cuenta los cambios de estado.

imagen

Luego iremos al archivo SceneDelegate.swift y agregaremos el c贸digo al m茅todo:

func scene(
        _ scene: UIScene,
        willConnectTo session: UISceneSession,
        options connectionOptions: UIScene.ConnectionOptions
    ) {
        let contentView = ContentView().environmentObject(Store())
        
        if let windowScene = scene as? UIWindowScene {
            let window = UIWindow(windowScene: windowScene)
            window.rootViewController = UIHostingController(rootView: contentView)
            self.window = window
            window.makeKeyAndVisible()
        }
    }

Del mismo modo, cualquier @EnvironmentObject se puede pasar a cualquier representaci贸n secundaria en toda la aplicaci贸n, y todo esto es posible gracias al entorno. La variable IsAddingMode est谩 marcadaEstadoe indica si la vista secundaria se muestra o no. WorkoutListView hereda autom谩ticamente la variable de la tienda , y no necesitamos pasarla expl铆citamente, pero debemos hacer esto para AddWorkoutView , porque se presenta en forma de una hoja que no es un elemento secundario de ContentView . Ahora cree un WorkoutListView que heredar谩 de View. Cree un nuevo archivo r谩pido llamado WorkoutListView .



struct WorkoutListView: View {
    @EnvironmentObject var store: Store
    
    var body: some View {
        List {
            ForEach(store.state.workouts) {
                WorkoutView(workout: $0)
            }
            .onDelete {
                self.store.dispatch(action: .removeWorkout(at: $0))
            }
            .listRowInsets(EdgeInsets())
        }
    }
}

Ver, que utiliza el elemento Lista de contenedores para mostrar una lista de entrenamientos. La funci贸n onDelete se utiliza para eliminar un entrenamiento y utiliza la acci贸n removeWorkout , que se realiza mediante la funci贸n de env铆o proporcionada por la tienda . Para mostrar el entrenamiento en la lista, se utiliza WorkoutView.

Cree otro archivo WorkoutView.swift que ser谩 responsable de mostrar nuestro elemento en la lista.

struct WorkoutView: View {
    let workout: Workout
    
    private var backgroundColor: Color {
        switch workout.complexity {
        case .low:
            return .green
        case .medium:
            return .orange
        case .high:
            return .red
        }
    }
    
    var body: some View {
        HStack {
            VStack(alignment: .leading) {
                Text(workout.name)
                Text("Distance:" + workout.distance + "km")
                    .font(.subheadline)
            }
            Spacer()
            VStack(alignment: .leading) {
                Text(simpleFormat(workout.date))
            }
        }
        .padding()
        .background(backgroundColor)
    }
}

private extension WorkoutView {
    func simpleFormat(_ date: Date) -> String {
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "dd MMM yyyy"
        dateFormatter.locale = .init(identifier: "en_GB")
        return dateFormatter.string(from: date)
    }
}

Esta vista toma el objeto de entrenamiento como par谩metro y se configura en funci贸n de sus propiedades.

Para agregar un nuevo elemento a la lista, debe cambiar el par谩metro isAddingMode a verdadero para mostrar AddWorkoutView . Esta responsabilidad recae en AddButton .

struct AddButton: View {
    @Binding var isAddingMode: Bool
    
    var body: some View {
        Button(action: { self.isAddingMode = true }) {
            Image(systemName: "plus")
        }
    }
}

AddButton tambi茅n vale la pena ponerlo en un archivo separado.

Esta vista es un bot贸n simple que se ha extra铆do del ContentView principal para una mejor estructura y separaci贸n de c贸digo.

Cree una vista para agregar un nuevo entrenamiento. Cree un nuevo archivo AddWorkoutView.swift :

struct AddWorkoutView: View {
    @EnvironmentObject private var store: Store
    
    @State private var nameText: String = ""
    @State private var distanceText: String = ""
    @State private var complexityField: Complexity = .medium
    @State private var dateField: Date = Date()
    @Binding var isAddingMode: Bool
    
    var body: some View {
        NavigationView {
            Form {
                TextField("Name", text: $nameText)
                TextField("Distance", text: $distanceText)
                Picker(selection: $complexityField, label: Text("Complexity")) {
                    Text("Low").tag(Complexity.low)
                    Text("Medium").tag(Complexity.medium)
                    Text("High").tag(Complexity.high)
                }
                DatePicker(selection: $dateField, displayedComponents: .date) {
                    Text("Date")
                }
            }
            .navigationBarTitle("Workout Details", displayMode: .inline)
            .navigationBarItems(
                leading: Button(action: { self.isAddingMode = false }) {
                    Text("Cancel")
                },
                trailing: Button(action: {
                    let workout = Workout(
                        name: self.nameText,
                        distance: self.distanceText,
                        date: self.dateField,
                        complexity: self.complexityField
                    )
                    self.store.dispatch(action: .addWorkout(workout))
                    self.isAddingMode = false
                }) {
                    Text("Save")
                }
                .disabled(nameText.isEmpty)
            )
        }
    }
}

Este es un controlador bastante grande que, como otros controladores, contiene la variable store. Tambi茅n contiene las variables nameText, distanceText, complexField e isAddingMode . Las primeras tres variables son necesarias para vincular TextField, Picker, DatePicker , que se puede ver en esta pantalla. La barra de navegaci贸n tiene dos elementos. El primer bot贸n es un bot贸n que cierra la pantalla sin agregar un nuevo entrenamiento, y el 煤ltimo agrega un nuevo entrenamiento a la lista, que se logra enviando la acci贸n addWorkout. Esta acci贸n tambi茅n cierra la pantalla de agregar nuevo entrenamiento.

imagen

Por 煤ltimo, pero no menos importante, es TrailingView .

struct TrailingView: View {
    @EnvironmentObject var store: Store
    
    var body: some View {
        HStack(alignment: .center, spacing: 30) {
            Button(action: {
                switch self.store.state.sortType {
                case .distance:
                    self.store.dispatch(action: .sort(by: .distance))
                default:
                    self.store.dispatch(action: .sort(by: .complexity))
                }
            }) {
                Image(systemName: "arrow.up.arrow.down")
            }
            EditButton()
        }
    }
}

Esta vista consta de dos botones que se encargan de ordenar la lista de ejercicios y de activar o desactivar el modo de edici贸n de nuestra lista de ejercicios. Las acciones de clasificaci贸n se llaman mediante la funci贸n de despacho, que podemos llamar a trav茅s de la propiedad de la tienda.

imagen

Resultado


La aplicaci贸n est谩 lista y deber铆a funcionar exactamente como se esperaba. Intentemos compilarlo y ejecutarlo.

recomendaciones


Redux y SwiftUI funcionan muy bien juntos. El c贸digo escrito con estas herramientas es f谩cil de entender y puede estar bien organizado. Otro buen aspecto de esta soluci贸n es su excelente capacidad de prueba de c贸digo. Sin embargo, esta soluci贸n no est谩 exenta de inconvenientes, uno de ellos es una gran cantidad de memoria utilizada por la aplicaci贸n cuando el estado de la aplicaci贸n es muy complejo, y el rendimiento de la aplicaci贸n puede no ser ideal en algunos escenarios espec铆ficos, ya que todas las vistas en SwiftUI se actualizan al crear un nuevo estado. Estas deficiencias pueden tener un gran impacto en la calidad de la aplicaci贸n y la interacci贸n del usuario, pero si las recordamos y preparamos el estado de manera razonable, el impacto negativo se puede minimizar o incluso evitar f谩cilmente.

Puede descargar o ver el c贸digo fuente del proyecto.Aqu铆 .

Espero que te haya gustado este art铆culo y hayas aprendido algo nuevo por ti mismo, nos vemos pronto. Adem谩s ser谩 a煤n m谩s interesante.

All Articles