
Esta semana hablaremos sobre la creación de un contenedor de estado similar al utilizado por
Redux . Es la única fuente de valor para la aplicación en desarrollo. Un solo estado para toda la aplicación facilita la depuración y la verificación. Una sola fuente de valores de verdad elimina miles de errores que ocurren al crear múltiples estados en una aplicación.
Una sola fuente de valores de verdad.
La idea principal es describir el estado de toda la aplicación a través de una sola estructura o composición de estructuras. Digamos que estamos trabajando en la creación de una aplicación de búsqueda de repositorio de Github, donde el estado es una matriz de repositorios que seleccionamos de acuerdo con una solicitud específica utilizando la API de Github.
struct AppState { var searchResult: [Repo] = [] }
El siguiente paso es pasar el estado (solo lectura) a cada vista dentro de la aplicación. La mejor manera de lograr esto es usar el entorno de SwiftUI. También puede pasar un objeto que contenga el estado de toda la aplicación al entorno de la vista base. La vista base compartirá el entorno con todas las vistas secundarias. Para obtener más información sobre el medio ambiente de SwiftUI, consulte el
poder del medio ambiente en la publicación SwiftUI .
final class Store: ObservableObject { @Published private(set) var state: AppState }
En el ejemplo anterior, creamos un objeto de
tienda , que almacena el estado de la aplicación y proporciona acceso de solo lectura. La propiedad State utiliza el contenedor de la propiedad
@Published , que notifica a SwiftUI de cualquier cambio. Le permite actualizar constantemente toda la aplicación, derivando de una sola fuente de valores de verdad. Anteriormente, hablamos sobre objetos de almacenamiento en artículos anteriores. Para obtener más información sobre esto, debe leer el artículo "
Modelado del estado de la aplicación utilizando objetos de tienda en SwiftUI ".
Reductor y acciones
Es hora de hablar sobre las acciones del usuario que conducen a cambios de estado. Una acción es una simple enumeración o colección de enumeraciones que describe un cambio de estado. Por ejemplo, establezca el valor de carga durante el muestreo de datos, asigne los repositorios resultantes a una propiedad de estado, etc. Ahora considere el código de muestra para una enumeración de Action.
enum AppAction { case search(query: String) case setSearchResult(repos: [Repo]) }
Reductor es una función que toma el estado actual, aplica una acción al estado y genera un nuevo estado. Por lo general, el
reductor o
composición de los reductores es el único lugar en una aplicación en la que cambia el estado. El hecho de que una sola función pueda cambiar el estado completo de una aplicación hace que el código sea muy simple, fácil de probar y fácil de depurar. El siguiente es un ejemplo de una función de reducción.
struct Reducer<State, Action> { let reduce: (inout State, Action) -> Void } let appReducer: Reducer<AppState, AppAction> = Reducer { state, action in switch action { case let .setSearchResults(repos): state.searchResult = repos } }
Flujo unidireccional
Ahora es el momento de hablar sobre el flujo de datos. Cada vista tiene acceso de solo lectura al estado a través del objeto de tienda. Las vistas pueden enviar acciones al objeto del repositorio. El reductor cambia de estado, y luego SwiftUI notifica todas las vistas de cambios de estado. SwiftUI tiene un algoritmo de comparación súper eficiente, por lo que mostrar el estado de toda la aplicación y actualizar las vistas modificadas es muy rápido.
Estado -> Ver -> Acción -> Estado -> VerEsta arquitectura solo funciona alrededor de un flujo de datos unidireccional. Esto significa que todos los datos en la aplicación siguen el mismo patrón, lo que hace que la lógica de la aplicación creada sea más predecible y más fácil de entender. Cambiemos el objeto de la tienda para admitir las acciones de envío.
final class Store<State, Action>: ObservableObject { @Published private(set) var state: State private let appReducer: Reducer<State, Action> init(initialState: State, appReducer: @escaping Reducer<State, Action>) { self.state = initialState self.appReducer = appReducer } func send(_ action: Action) { appReducer.reduce(&state, action) } }
Efectos secundarios
Ya hemos implementado un flujo
unidireccional que acepta acciones del usuario y cambios de estado, pero ¿qué pasa con una acción asincrónica, que generalmente llamamos
efectos secundarios ? ¿Cómo agregar soporte de tareas asíncronas para el tipo de almacenamiento utilizado? Creo que es hora de introducir el uso de
Combine Framework , que es ideal para manejar tareas asincrónicas.
import Foundation import Combine protocol Effect { associatedtype Action func mapToAction() -> AnyPublisher<Action, Never> } enum SideEffect: Effect { case search(query: String) func mapToAction() -> AnyPublisher<Action, Never> { switch self { case let .search(query): return dependencies.githubService .searchPublisher(matching: query) .replaceError(with: []) .map { AppAction.setSearchResults(repos: $0) } .eraseToAnyPublisher() } } }
Agregamos soporte para
tareas asíncronas al introducir el protocolo Effect.
El efecto es una secuencia de acciones que se puede publicar utilizando el tipo de
editor del marco de combinación . Esto le permite procesar trabajos asincrónicos con Combine y luego publicar acciones que el reductor usará para aplicar acciones al estado actual.
final class Store<State, Action>: ObservableObject { @Published private(set) var state: State private let appReducer: Reducer<State, Action> private var cancellables: Set<AnyCancellable> = [] init(initialState: State, appReducer: Reducer<State, Action>) { self.state = initialState self.appReducer = appReducer } func send(_ action: Action) { appReducer.reduce(&state, action) } func send<E: Effect>(_ effect: E) where E.Action == Action { effect .mapToAction() .receive(on: DispatchQueue.main) .sink(receiveValue: send) .store(in: &cancellables) } }
Ejemplo práctico
Finalmente, podemos completar la aplicación de búsqueda de repositorio, que llama asíncronamente a la
API de Github y selecciona los repositorios que coinciden con la consulta. El código fuente completo de la aplicación está disponible en
Github .
struct SearchContainerView: View { @EnvironmentObject var store: Store<AppState, AppAction> @State private var query: String = "Swift" var body: some View { SearchView( query: $query, repos: store.state.searchResult, onCommit: fetch ).onAppear(perform: fetch) } private func fetch() { store.send(SideEffect.search(query: query)) } } struct SearchView : View { @Binding var query: String let repos: [Repo] let onCommit: () -> Void var body: some View { NavigationView { List { TextField("Type something", text: $query, onCommit: onCommit) if repos.isEmpty { Text("Loading...") } else { ForEach(repos) { repo in RepoRow(repo: repo) } } }.navigationBarTitle(Text("Search")) } } }
Divida la pantalla en dos vistas:
Vista de contenedor y
Vista de representación .
La vista de contenedor controla las acciones y selecciona las partes necesarias del
estado global .
La Vista de representación recibe datos y los muestra. Ya hablamos sobre Vistas de contenedores en artículos anteriores, para obtener más información, siga el enlace "
Presentación de vistas de contenedores en SwiftUI "
Conclusiones
Hoy aprendimos a crear un
contenedor de estado similar a Redux con efectos secundarios en mente . Para hacer esto, utilizamos la función SwiftUI Environment y el marco Combinar. Espero que este artículo haya sido útil.
Gracias por leer, y hasta pronto!