Redux são como contêineres de estado no SwiftUI. O básico

imagem

Nesta semana, falaremos sobre a criação de um contêiner de estado semelhante ao usado pelo Redux . É a única fonte de valor para o aplicativo em desenvolvimento. Um único estado para todo o aplicativo facilita a depuração e a verificação. Uma única fonte de valores verdadeiros elimina milhares de erros que ocorrem ao criar vários estados em um aplicativo.

Uma única fonte de valores de verdade


A idéia principal é descrever o estado de todo o aplicativo por meio de uma única estrutura ou composição de estruturas. Digamos que estamos trabalhando na criação de um aplicativo de pesquisa de repositório do Github, em que state é uma matriz de repositórios que selecionamos de acordo com uma solicitação específica usando a API do Github.

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

O próximo passo é passar o estado (somente leitura) para cada visualização no aplicativo. A melhor maneira de fazer isso é usar o ambiente do SwiftUI. Você também pode passar um objeto que contém o estado de todo o aplicativo para o Ambiente da visualização base. A vista base compartilhará o ambiente com todas as vistas filho. Para saber mais sobre o ambiente da SwiftUI, consulte a publicação Power of the Environment na SwiftUI .

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

No exemplo acima, criamos um objeto de armazenamento , que armazena o estado do aplicativo e fornece acesso somente leitura a ele. A propriedade State usa o wrapper da propriedade @Published , que notifica o SwiftUI sobre quaisquer alterações. Ele permite que você atualize constantemente todo o aplicativo, derivando-o de uma única fonte de valores de verdade. Anteriormente, falamos sobre objetos de armazenamento em artigos anteriores.Para saber mais sobre isso, você precisa ler o artigo " Modelando o estado do aplicativo usando objetos de armazenamento no SwiftUI ".

Redutor e Ações


É hora de falar sobre ações do usuário que levam a alterações de estado. Uma ação é uma enumeração simples ou coleção de enumerações que descrevem uma alteração de estado. Por exemplo, defina o valor da carga durante a amostragem de dados, atribua os repositórios resultantes a uma propriedade state, etc. Agora considere o código de exemplo para uma enumeração de ação.

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

Redutor é uma função que assume o estado atual, aplica uma ação ao estado e gera um novo estado. Normalmente, redutor ou composição de redutores é o único local em um aplicativo em que o estado muda. O fato de uma única função poder alterar todo o estado de um aplicativo torna o código muito simples, fácil de testar e fácil de depurar. A seguir, é apresentado um exemplo de uma função de redução.

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

Fluxo unidirecional


Agora é hora de falar sobre o fluxo de dados. Cada visualização tem acesso somente leitura ao estado através do objeto de armazenamento. As visualizações podem enviar ações para o objeto de repositório. O redutor muda de estado e o SwiftUI notifica todas as visualizações de alterações de estado. O SwiftUI possui um algoritmo de comparação supereficiente, portanto é muito rápido exibir o estado de todo o aplicativo e atualizar as visualizações alteradas.

Estado -> Ver -> Ação -> Estado -> Ver

Essa arquitetura funciona apenas em torno de um fluxo de dados unidirecional. Isso significa que todos os dados no aplicativo seguem o mesmo padrão, o que torna a lógica do aplicativo criado mais previsível e mais fácil de entender. Vamos alterar o objeto de armazenamento para suportar o envio de ações.

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

Efeitos colaterais


Já implementamos um fluxo unidirecional que aceita ações do usuário e altera o estado, mas e uma ação assíncrona, que geralmente chamamos de efeitos colaterais . Como adicionar suporte a tarefas assíncronas para o tipo de armazenamento usado? Acho que é hora de introduzir o uso do Combine Framework , ideal para lidar com tarefas assíncronas.

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

Adicionamos suporte para tarefas assíncronas com a introdução do protocolo Effect. Effect é uma sequência de ações que pode ser publicada usando o tipo de editor da estrutura do Combine . Isso permite processar tarefas assíncronas com Combinar e publicar ações que o redutor usará para aplicar ações ao estado atual.

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

Exemplo prático


Por fim, podemos concluir o aplicativo de pesquisa de repositório, que chama assincronamente a API do Github e seleciona os repositórios que correspondem à consulta. O código fonte completo do aplicativo está disponível no 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 a tela em duas visualizações: Exibição de Contêiner e Exibição de Renderização . A Visualização de Contêiner controla as ações e seleciona as partes necessárias do estado global . O Rendering View recebe dados e os exibe. Já falamos sobre visualizações de contêineres em artigos anteriores, para saber mais, siga o link " Introdução às visualizações de contêineres no SwiftUI "

Conclusões


Hoje aprendemos como criar um contêiner de estado semelhante ao Redux com efeitos colaterais em mente . Para fazer isso, usamos a função SwiftUI Environment e a estrutura Combine. Espero que este artigo tenha sido útil.

Obrigado pela leitura e até breve!

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


All Articles