A Composable Architecture (TCA, abreviadamente) é uma biblioteca para construir aplicações de uma forma consistente e compreensível, com composição, testes e ergonomia em mente. Ele pode ser usado em SwiftUI, UIKit e muito mais, e em qualquer plataforma Apple (iOS, macOS, visionOS, tvOS e watchOS).
O que é a arquitetura combinável?
Saber mais
Exemplos
Uso básico
Documentação
Comunidade
Instalação
Traduções
Esta biblioteca fornece algumas ferramentas básicas que podem ser usadas para construir aplicativos de finalidades e complexidades variadas. Ele fornece histórias convincentes que você pode seguir para resolver muitos problemas que você encontra no dia a dia ao criar aplicativos, como:
Gestão estadual
Como gerenciar o estado do seu aplicativo usando tipos de valores simples e compartilhar o estado em várias telas para que as mutações em uma tela possam ser imediatamente observadas em outra tela.
Composição
Como dividir recursos grandes em componentes menores que podem ser extraídos em seus próprios módulos isolados e facilmente colados novamente para formar o recurso.
Efeitos colaterais
Como permitir que certas partes do aplicativo se comuniquem com o mundo exterior da maneira mais testável e compreensível possível.
Teste
Como não apenas testar um recurso construído na arquitetura, mas também escrever testes de integração para recursos que foram compostos de muitas partes e escrever testes ponta a ponta para entender como os efeitos colaterais influenciam seu aplicativo. Isso permite que você tenha fortes garantias de que sua lógica de negócios está funcionando da maneira esperada.
Ergonomia
Como realizar tudo isso em uma API simples com o mínimo de conceitos e partes móveis possível.
A Composable Architecture foi projetada ao longo de vários episódios de Point-Free, uma série de vídeos que explora a programação funcional e a linguagem Swift, apresentada por Brandon Williams e Stephen Celis.
Você pode assistir a todos os episódios aqui, bem como um tour dedicado e com várias partes da arquitetura do zero.
Este repositório vem com muitos exemplos para demonstrar como resolver problemas comuns e complexos com a Composable Architecture. Confira este diretório para ver todos eles, incluindo:
Estudos de caso
Começando
Efeitos
Navegação
Redutores de ordem superior
Componentes reutilizáveis
Gerente de localização
Gerenciador de movimento
Procurar
Reconhecimento de fala
Aplicativo SyncUps
Jogo da velha
Todos
Memorandos de voz
Procurando por algo mais substancial? Confira o código-fonte do isowords, um jogo de busca de palavras para iOS construído em SwiftUI e Composable Architecture.
Observação
Para um tutorial interativo passo a passo, não deixe de conferir Conheça a Arquitetura Composable.
Para construir um recurso usando a Composable Architecture você define alguns tipos e valores que modelam seu domínio:
Estado : um tipo que descreve os dados que seu recurso precisa para executar sua lógica e renderizar sua IU.
Action : um tipo que representa todas as ações que podem acontecer no seu recurso, como ações do usuário, notificações, fontes de eventos e muito mais.
Redutor : uma função que descreve como evoluir o estado atual do aplicativo para o próximo estado, dada uma ação. O redutor também é responsável por retornar quaisquer efeitos que devam ser executados, como solicitações de API, o que pode ser feito retornando um valor Effect
.
Store : o tempo de execução que realmente orienta seu recurso. Você envia todas as ações do usuário para a loja para que a loja possa executar o redutor e os efeitos, e você pode observar as mudanças de estado na loja para que possa atualizar a IU.
Os benefícios de fazer isso são que você desbloqueará instantaneamente a testabilidade de seu recurso e poderá dividir recursos grandes e complexos em domínios menores que podem ser agrupados.
Como exemplo básico, considere uma IU que mostra um número junto com os botões "+" e "-" que aumentam e diminuem o número. Para tornar as coisas interessantes, suponha que haja também um botão que, quando tocado, faz uma solicitação de API para buscar um fato aleatório sobre esse número e exibi-lo na visualização.
Para implementar esse recurso criamos um novo tipo que abrigará o domínio e o comportamento do recurso, e será anotado com a macro @Reducer
:
importar recurso ComposableArchitecture@Reducerstruct {}
Aqui precisamos definir um tipo para o estado do recurso, que consiste em um inteiro para a contagem atual, bem como uma string opcional que representa o fato que está sendo apresentado:
@Reducerstruct Feature { @ObservableState struct State: Equatable { var count = 0 var numberFact: String? }}
Observação
Aplicamos a macro @ObservableState
ao State
para aproveitar as vantagens das ferramentas de observação da biblioteca.
Também precisamos definir um tipo para as ações do recurso. Existem ações óbvias, como tocar no botão de diminuir, no botão de incrementar ou no botão de fatos. Mas também existem alguns que não são óbvios, como a ação que ocorre quando recebemos uma resposta da solicitação da API de fato:
@Reducerstruct Feature { @ObservableState struct State: Equatable { /* ... */ } enum Action { case decrementButtonTapped case incrementButtonTapped case numberFactButtonTapped case numberFactResponse(String) }}
E então implementamos a propriedade body
, que é responsável por compor a lógica e o comportamento reais do recurso. Nele podemos usar o redutor Reduce
para descrever como mudar o estado atual para o próximo estado e quais efeitos precisam ser executados. Algumas ações não precisam executar efeitos e podem retornar .none
para representar isso:
@Reducerstruct Feature { @ObservableState struct State: Equatable { /* ... */ } enum Action { /* ... */ } var body: some Redutor{ Reduzir { estado, ação em ação de troca { case .decrementButtonTapped: state.count -= 1 retorno .none case .incrementButtonTapped: state.count += 1 return .none case .numberFactButtonTapped: return .run { [count = state.count] enviar let (data, _) = tentar aguardar URLSession.shared.data ( de: URL(string: "http://numbersapi.com/(count)/trivia")! ) aguardar envio( .numberFactResponse(String(decodificação: dados, como: UTF8.self)) ) } case let .numberFactResponse(fato): state.numberFact = retorno de fato .none } } }}
E finalmente definimos a visualização que exibe o recurso. Ele mantém um StoreOf
para que possa observar todas as alterações no estado e renderizar novamente, e podemos enviar todas as ações do usuário para a loja para que o estado mude:
struct FeatureView: View { let store: StoreOfvar body: some View { Form { Section { Text("(store.count)") Button("Decrement") { store.send(.decrementButtonTapped) } Button( "Incremento") { store.send(.incrementButtonTapped) } } Seção { Button("Número fato") { store.send(.numberFactButtonTapped) } } if let fact = store.numberFact { Text(fact) } } }}
Também é simples ter um controlador UIKit retirado desta loja. Você pode observar mudanças de estado na loja em viewDidLoad
e, em seguida, preencher os componentes da UI com dados da loja. O código é um pouco mais longo que a versão do SwiftUI, então o resumimos aqui:
class FeatureViewController: UIViewController { let store: StoreOfinit(store: StoreOf ) { self.store = store super.init(nibName: nil, bundle: nil) } init obrigatório?(codificador: NSCoder) { fatalError("init(coder:) não foi implementado") } substituir func viewDidLoad() { super.viewDidLoad() deixe countLabel = UILabel() deixe decrementButton = UIButton() deixe incrementButton = UIButton() deixe factLabel = UILabel() // Omitido: Adicionar subvisões e configurar restrições... observe {[eu fraco] em guarda deixe-se senão {retornar} countLabel.text = "(self.store.text)" factLabel.text = self.store.numberFact } } @objc private func incrementButtonTapped() { self.store.send(.incrementButtonTapped) } @objc private func decrementButtonTapped() { self.store.send(.decrementButtonTapped) } @objc private func factButtonTapped() { self.store.send(.numberFactButtonTapped) }}
Quando estivermos prontos para exibir esta visualização, por exemplo, no ponto de entrada do aplicativo, podemos construir uma loja. Isso pode ser feito especificando o estado inicial para iniciar o aplicativo, bem como o redutor que alimentará o aplicativo:
importar ComposableArchitecture@mainstruct MyApp: App { var body: some Scene { WindowGroup { FeatureView( armazenar: Store(initialState: Feature.State()) { Feature() } ) } }}
E isso é o suficiente para colocar algo na tela para brincar. Definitivamente, são mais algumas etapas do que se você fizesse isso no estilo básico do SwiftUI, mas há alguns benefícios. Isso nos dá uma maneira consistente de aplicar mutações de estado, em vez de dispersar a lógica em alguns objetos observáveis e em vários fechamentos de ação de componentes de UI. Também nos dá uma maneira concisa de expressar os efeitos colaterais. E podemos testar imediatamente essa lógica, incluindo os efeitos, sem fazer muito trabalho adicional.
Observação
Para obter informações mais detalhadas sobre testes, consulte o artigo dedicado sobre testes.
Para testar use um TestStore
, que pode ser criado com as mesmas informações do Store
, mas faz um trabalho extra para permitir que você afirme como seu recurso evolui à medida que as ações são enviadas:
@Testfunc basics() async { let store = TestStore(initialState: Feature.State()) { Feature() }}
Depois que o armazenamento de teste for criado, podemos usá-lo para fazer uma asserção de todo um fluxo de etapas do usuário. Cada passo do caminho precisamos provar que o estado mudou como esperávamos. Por exemplo, podemos simular o fluxo do usuário ao tocar nos botões de incremento e decremento:
// Teste se tocar nos botões de incremento/decremento altera o countawait store.send(.incrementButtonTapped) { $0.count = 1}aguardar store.send(.decrementButtonTapped) { $0.contagem = 0}
Além disso, se uma etapa causa a execução de um efeito, que realimenta os dados no armazenamento, devemos afirmar isso. Por exemplo, se simularmos o usuário tocando no botão de fato, esperamos receber uma resposta de fato com o fato, o que faz com que o estado numberFact
seja preenchido:
aguardar store.send(.numberFactButtonTapped)aguardar store.receive(.numberFactResponse) { $0.númeroFato = ???}
Porém, como sabemos que fato nos será enviado de volta?
Atualmente nosso redutor está usando um efeito que chega ao mundo real para atingir um servidor API, e isso significa que não temos como controlar seu comportamento. Estamos à mercê da nossa conectividade com a Internet e da disponibilidade do servidor API para escrever este teste.
Seria melhor que essa dependência fosse passada para o redutor para que possamos usar uma dependência ativa ao executar o aplicativo em um dispositivo, mas usar uma dependência simulada para testes. Podemos fazer isso adicionando uma propriedade ao redutor Feature
:
@Reducerstruct Feature {let numberFact: (Int) lançamentos assíncronos -> String // ...}
Então podemos usá-lo na implementação reduce
:
caso .numberFactButtonTapped: return .run { [count = state.count] enviar deixe fato = tente aguardar self.numberFact(count) aguardar send(.numberFactResponse(fact)) }
E no ponto de entrada da aplicação podemos fornecer uma versão da dependência que realmente interage com o servidor API do mundo real:
@mainstruct MyApp: App { var body: some Scene { WindowGroup { FeatureView( armazenar: Store(initialState: Feature.State()) { Feature( numberFact: {número em let (data, _) = tente aguardar URLSession.shared.data( de: URL(string: "http://numbersapi.com/(number)")! ) return String (decodificação: dados, como: UTF8.self) } ) } ) } }}
Mas em testes podemos usar uma dependência simulada que retorna imediatamente um fato determinístico e previsível:
@Testfunc basics() async { let store = TestStore(initialState: Feature.State()) { Feature(numberFact: { "($0) é um bom número Brent" }) }}
Com esse pouco de trabalho inicial podemos finalizar o teste simulando o usuário tocando no botão fato, e então recebendo a resposta da dependência para apresentar o fato:
aguardar store.send(.numberFactButtonTapped)aguardar store.receive(.numberFactResponse) { $0.numberFact = "0 é um bom número, Brent"}
Também podemos melhorar a ergonomia do uso da dependência numberFact
em nosso aplicativo. Com o tempo, o aplicativo pode evoluir para muitos recursos, e alguns desses recursos também podem querer acesso a numberFact
, e passá-lo explicitamente por todas as camadas pode ser irritante. Existe um processo que você pode seguir para “registrar” dependências na biblioteca, disponibilizando-as instantaneamente para qualquer camada da aplicação.
Observação
Para obter informações mais detalhadas sobre gerenciamento de dependências, consulte o artigo dedicado sobre dependências.
Podemos começar agrupando a funcionalidade de fato numérico em um novo tipo:
struct NumberFactClient { var fetch: (Int) lançamentos assíncronos -> String}
E então registrar esse tipo no sistema de gerenciamento de dependências, conformando o cliente ao protocolo DependencyKey
, que exige que você especifique o valor ativo a ser usado ao executar o aplicativo em simuladores ou dispositivos:
extensão NumberFactClient: DependencyKey { static let liveValue = Self ( buscar: { número em let (dados, _) = tente aguardar URLSession.shared .data(from: URL(string: "http://numbersapi.com/(número)")! ) return String(decodificação: dados, como: UTF8.self) } )}extensão DependencyValues { var numberFact: NumberFactClient { get { self[NumberFactClient.self] } set { self[NumberFactClient.self] = novoValor } }}
Com esse pouco de trabalho inicial feito, você pode começar instantaneamente a usar a dependência em qualquer recurso usando o wrapper de propriedade @Dependency
:
@Redutor struct Feature {- deixe numberFact: (Int) lançamentos assíncronos -> String + @Dependency(.numberFact) var numberFact …- tente aguardar self.numberFact(count)+ tente aguardar self.numberFact.fetch(count) }
Este código funciona exatamente como antes, mas você não precisa mais passar explicitamente a dependência ao construir o redutor do recurso. Ao executar o aplicativo em visualizações, no simulador ou em um dispositivo, a dependência live será fornecida ao redutor, e nos testes a dependência de teste será fornecida.
Isso significa que o ponto de entrada do aplicativo não precisa mais construir dependências:
@mainstruct MyApp: App { var body: some Scene { WindowGroup { FeatureView( armazenar: Store(initialState: Feature.State()) { Feature() } ) } }}
E o armazenamento de teste pode ser construído sem especificar nenhuma dependência, mas você ainda pode substituir qualquer dependência necessária para o propósito do teste:
deixe armazenar = TestStore(initialState: Feature.State()) { Feature()} withDependencies: { $0.numberFact.fetch = { "($0) é um bom número Brent" }}// ...
Esse é o básico para construir e testar um recurso na Composable Architecture. Há muito mais coisas a serem exploradas, como composição, modularidade, adaptabilidade e efeitos complexos. O diretório Exemplos tem vários projetos para explorar para ver usos mais avançados.
A documentação para releases e main
está disponível aqui:
main
1.17.0 (guia de migração)
1.16.0 (guia de migração)
1.15.0 (guia de migração)
1.14.0 (guia de migração)
1.13.0 (guia de migração)
1.12.0 (guia de migração)
1.11.0 (guia de migração)
1.10.0 (guia de migração)
1.9.0 (guia de migração)
1.8.0 (guia de migração)
1.7.0 (guia de migração)
1.6.0 (guia de migração)
1.5.0 (guia de migração)
1.4.0 (guia de migração)
1.3.0
1.2.0
1.1.0
1.0.0
0,59,0
0,58,0
0,57,0
Há vários artigos na documentação que podem ser úteis à medida que você se torna mais confortável com a biblioteca:
Começando
Dependências
Teste
Navegação
Estado de compartilhamento
Desempenho
Simultaneidade
Ligações
Se você quiser discutir a Arquitetura Composable ou tiver alguma dúvida sobre como usá-la para resolver um problema específico, há vários lugares onde você pode discutir com outros entusiastas do Point-Free:
Para discussões longas, recomendamos a guia de discussões deste repositório.
Para bate-papo casual, recomendamos a folga da comunidade Point-Free.
Você pode adicionar ComposableArchitecture a um projeto Xcode adicionando-o como uma dependência de pacote.
No menu Arquivo , selecione Adicionar dependências de pacote...
Digite "https://github.com/pointfreeco/swift-composable-architecture" no campo de texto do URL do repositório de pacotes
Dependendo de como seu projeto está estruturado:
Se você tiver um único destino de aplicativo que precisa de acesso à biblioteca, adicione ComposableArchitecture diretamente ao seu aplicativo.
Se quiser usar esta biblioteca a partir de vários destinos Xcode ou combinar destinos Xcode e destinos SPM, você deverá criar uma estrutura compartilhada que dependa do ComposableArchitecture e, em seguida, depender dessa estrutura em todos os seus destinos. Para ver um exemplo disso, confira o aplicativo de demonstração Tic-Tac-Toe, que divide muitos recursos em módulos e consome a biblioteca estática dessa maneira usando o pacote Swift do jogo da velha .
A Composable Architecture foi construída tendo em mente a extensibilidade, e há diversas bibliotecas apoiadas pela comunidade disponíveis para aprimorar seus aplicativos:
Extras da Composable Architecture: uma biblioteca que acompanha a Composable Architecture.
TCAComposer: Uma estrutura macro para gerar código padrão na arquitetura Composable.
TCACoordinators: O padrão de coordenador na Composable Architecture.
Se você gostaria de contribuir com uma biblioteca, abra um PR com um link para ela!
As seguintes traduções deste README foram contribuídas por membros da comunidade:
árabe
Francês
hindi
indonésio
italiano
japonês
coreano
polonês
Português
russo
Chinês simplificado
Espanhol
ucraniano
Se você gostaria de contribuir com uma tradução, abra um PR com um link para um Gist!
Temos um artigo dedicado a todas as perguntas e comentários mais frequentes que as pessoas fazem sobre a biblioteca.
As seguintes pessoas deram feedback sobre a biblioteca em seus estágios iniciais e ajudaram a torná-la o que é hoje:
Paul Colton, Kaan Dedeoglu, Matt Diephouse, Josef Doležal, Eimantas, Matthew Johnson, George Kaimakas, Nikita Leonov, Christopher Liscio, Jeffrey Macko, Alejandro Martinez, Shai Mishali, Willis Plummer, Simon-Pierre Roy, Justin Price, Sven A. Schmidt , Kyle Sherman, Petr Šíma, Jasdev Singh, Maxim Smirnov, Ryan Stone, Daniel Hollis Tavares, e todos os assinantes do Point-Free?.
Agradecimentos especiais a Chris Liscio, que nos ajudou a resolver muitas peculiaridades estranhas do SwiftUI e ajudou a refinar a API final.
E obrigado a Shai Mishali e ao projeto CombineCommunity, do qual tiramos a implementação de Publishers.Create
, que usamos no Effect
para ajudar a unir APIs baseadas em delegação e retorno de chamada, tornando muito mais fácil a interface com estruturas de terceiros.
A Composable Architecture foi construída com base em ideias iniciadas por outras bibliotecas, em particular Elm e Redux.
Existem também muitas bibliotecas de arquitetura na comunidade Swift e iOS. Cada um deles tem seu próprio conjunto de prioridades e compensações que diferem da Arquitetura Composable.
Costelas
Laço
ReSwift
Fluxo de trabalho
Kit Reator
RxFeedback
Mobius.swift
Fluxor
Kit de arquitetura prometida
Esta biblioteca é lançada sob a licença do MIT. Consulte LICENÇA para obter detalhes.