
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 -> VerEssa 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!