Redux est comme des conteneurs d'état dans SwiftUI. Les bases

image

Cette semaine, nous parlerons de la création d'un conteneur d'état similaire à celui utilisé par Redux . C'est la seule source de valeur pour l'application en cours de développement. Un état unique pour l'ensemble de l'application facilite le débogage et la vérification. Une seule source de valeurs de vérité élimine des milliers d'erreurs qui se produisent lors de la création de plusieurs états dans une application.

Une seule source de valeurs de vérité


L'idée principale est de décrire l'état de l'ensemble de l'application à travers une seule structure ou composition de structures. Disons que nous travaillons sur la création d'une application de recherche de référentiel Github, où état est un tableau de référentiels que nous sélectionnons en fonction d'une demande spécifique à l'aide de l'API Github.

struct AppState { var searchResult: [Repo] = [] } 

L'étape suivante consiste à passer l'état (en lecture seule) à chaque vue de l'application. La meilleure façon d'y parvenir est d'utiliser l'environnement de SwiftUI. Vous pouvez également transmettre un objet contenant l'état de l'ensemble de l'application à l'environnement de la vue de base. La vue de base partagera l'environnement avec toutes les vues enfant. Pour en savoir plus sur la fonction d'environnement de SwiftUI, consultez Power of the Environment dans SwiftUI .

 final class Store: ObservableObject { @Published private(set) var state: AppState } 

Dans l'exemple ci-dessus, nous avons créé un objet de stockage , qui stocke l'état de l'application et lui fournit un accès en lecture seule. La propriété State utilise l'encapsuleur de la propriété @Published , qui informe SwiftUI de toute modification. Il vous permet de mettre à jour en permanence l'application entière, en la dérivant d'une seule source de valeurs de vérité. Plus tôt, nous avons parlé des objets de stockage dans les articles précédents. Pour en savoir plus, vous devez lire l'article « Modélisation de l'état d'une application à l'aide d'objets de magasin dans SwiftUI ».

Réducteur et actions


Il est temps de parler des actions des utilisateurs qui conduisent à des changements d'état. Une action est une énumération simple ou une collection d'énumérations décrivant un changement d'état. Par exemple, définissez la valeur de charge pendant l'échantillonnage des données, affectez les référentiels résultants à une propriété d'état, etc. Considérez maintenant l'exemple de code pour une énumération d'Action.

 enum AppAction { case search(query: String) case setSearchResult(repos: [Repo]) } 

Le réducteur est une fonction qui prend l'état actuel, applique une action à l'état et génère un nouvel état. Généralement, le réducteur ou la composition des réducteurs est le seul endroit dans une application où l'état change. Le fait qu'une seule fonction puisse changer l'état complet d'une application rend le code très simple, facile à tester et facile à déboguer. Voici un exemple de fonction de réduction.

 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 } } 

Flux unidirectionnel


Il est maintenant temps de parler du flux de données. Chaque vue a un accès en lecture seule à l'état via l'objet de magasin. Les vues peuvent envoyer des actions à l'objet de référentiel. Le réducteur change d'état, puis SwiftUI notifie toutes les vues des changements d'état. SwiftUI a un algorithme de comparaison super efficace, donc afficher l'état de toute l'application et mettre à jour les vues modifiées est très rapide.

État -> Affichage -> Action -> État -> Affichage

Cette architecture fonctionne uniquement autour d'un flux de données unidirectionnel. Cela signifie que toutes les données de l'application suivent le même modèle, ce qui rend la logique de l'application créée plus prévisible et plus facile à comprendre. Modifions l'objet de magasin pour prendre en charge la soumission d'actions.

 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) } } 

Effets secondaires


Nous avons déjà implémenté un flux unidirectionnel qui accepte les actions de l'utilisateur et change d'état, mais qu'en est-il d'une action asynchrone, que nous appelons généralement des effets secondaires . Comment ajouter la prise en charge des tâches asynchrones pour le type de stockage utilisé? Je pense qu'il est temps d'introduire l'utilisation du Framework Combine , qui est idéal pour gérer les tâches asynchrones.

 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() } } } 

Nous avons ajouté la prise en charge des tâches asynchrones en introduisant le protocole Effect. L'effet est une séquence d'actions qui peut être publiée à l'aide du type d' éditeur du framework Combine . Cela vous permet de traiter des travaux asynchrones à l'aide de Combine, puis de publier des actions que le réducteur utilisera pour appliquer des actions à l'état actuel.

 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) } } 

Exemple pratique


Enfin, nous pouvons compléter l'application de recherche de référentiel, qui appelle de manière asynchrone l' API Github et sélectionne les référentiels correspondant à la demande. Le code source complet de l'application est disponible sur 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")) } } } 

Divisez l'écran en deux vues: vue Conteneur et vue Rendu . La vue Conteneur contrôle les actions et sélectionne les pièces nécessaires dans l' état global . La vue de rendu reçoit des données et les affiche. Nous avons déjà parlé des vues de conteneur dans les articles précédents, pour en savoir plus, suivez le lien « Présentation des vues de conteneur dans SwiftUI »

Conclusions


Aujourd'hui, nous avons appris à créer un conteneur d'état de type Redux avec les effets secondaires à l'esprit . Pour ce faire, nous avons utilisé la fonction d'environnement SwiftUI et le framework Combine. J'espère que cet article vous a été utile.

Merci d'avoir lu et à bientôt!

Source: https://habr.com/ru/post/fr476398/


All Articles