The Composable Architecture (ou simplesmente TCA) é uma biblioteca para construir aplicativos de forma consistente e compreensível, tendo em mente composição, teste e ergonomia. Pode ser usado no SwiftUI, UIKit e em qualquer plataforma da Apple (iOS, macOS, tvOS e watchOS).
- O que é The Composable Architecture?
- Aprender mais
- Exemplos
- Uso básico
- Bibliotecas complementares
- FAQ
- Requisitos
- Instalação
- Documentação
- Ajuda
- Traduções
- Créditos e Agradecimentos
- Outras Bibliotecas
Essa biblioteca fornece algumas ferramentas básicas que podem ser usadas para criar aplicativos com várias finalidades e complexidades. Fornece casos de uso que você pode seguir para resolver muitos problemas que você encontra no dia-a-dia ao criar aplicativos, como:
-
Gerenciamento de estado
Como gerenciar o estado do seu aplicativo usando tipos de valor simples, e compartilhar o estado em várias telas para que as mutações em uma tela possam ser observadas imediatamente em outra tela. -
Composição
Como dividir grandes features em componentes menores que podem ser extraídos para seus próprios módulos isolados e facilmente colados de volta para formar a feature. -
Efeitos secundários
Como permitir que certas partes do aplicativo "conversem" com o mundo exterior da maneira mais testável e compreensível possível. -
Testes
Como não apenas testar uma feature construída na arquitetura, mas também escrever testes de integração para features que foram compostas de muitas partes e escrever testes de ponta a ponta para entender como os efeitos colaterais influenciam seu aplicativo. Isso permite que você garanta de maneira sólida que sua lógica de negócios está sendo executada da maneira que você espera. -
Ergonomia
Como realizar todos os itens acima em uma API simples com o mínimo possível de conceitos e partes móveis.
The Composable Architecture foi projetado ao longo de muitos episódios em Point-Free, uma série de vídeos explorando programação funcional e a linguagem Swift, hospedada por Brandon Williams e Stephen Celis.
Você pode assistir a todos os episódios aqui, bem como um tour dedicado em várias partes da arquitetura do zero: parte 1, parte 2, parte 3 e parte 4.
Este repositório tem muitos exemplos para demonstrar como resolver problemas comuns e complexos com a Composable Architecture. Confira o diretório aqui para ver todos eles, incluindo:
- Casos de Estudo
- Primeiros Passos
- Efeitos
- Navegação
- Reducers de ordem superior
- Componentes reutilizáveis
- Location manager
- Motion manager
- Pesquisar
- Reconhecimento de fala
- Tic-Tac-Toe
- Lista de coisas para fazer
- Notas de Voz
Procurando algo mais sério? Confira o código-fonte de isowords, um jogo de busca de palavras para iOS construído no SwiftUI e na Composable Architecture.
Para construir uma feature usando a Composable Architecture você define alguns tipos e valores que modelam seu domínio:
- Estado: Um tipo que descreve os dados que sua feature precisa para executar sua lógica e renderizar sua interface do usuário.
- Ação: Um tipo que representa todas as ações que podem ocorrer em sua feature, como ações do usuário, notificações, fontes de eventos e muito mais.
- Ambiente: Um tipo que contém todas as dependências que a feature precisa, como chamadas de API, análise, etc.
- Reducer: Uma função que descreve como evoluir o estado atual do aplicativo para o próximo estado de acordo com uma ação. O Reducer também é responsável por retornar quaisquer efeitos que devem ser executados, como chamadas de API, o que pode ser feito retornando um valor
Effect
. - Store: O lugar que armazena sua feature. Você envia todas as ações do usuário para a Store para que possa executar o Reducer e os efeitos, e assim poder observar as alterações de estado e atualizar a UI.
Os benefícios de fazer isso é que você desbloqueará instantaneamente a testabilidade da sua feature e poderá dividir features grandes e complexas em domínios menores que podem ser unidos.
Como exemplo básico, considere uma UI 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 também haja um botão que, quando tocado, faça uma chamada de API para buscar um fato aleatório sobre esse número e, em seguida, exiba o fato em um alerta.
O estado dessa feature consistiria em um inteiro para a contagem atual, bem como uma string opcional que representa o título do alerta que queremos mostrar (opcional porque nil
representa não mostrar um alerta):
struct AppState: Equatable {
var count = 0
var numberFactAlert: String?
}
Em seguida, temos as ações na feature. Existem as ações óbvias, como tocar no botão de decremento, no botão de incremento ou no botão de fato. Mas também existem alguns pouco óbvios, como a ação do usuário dispensando o alerta e a ação que ocorre quando recebemos uma resposta da chamada da API de fato:
enum AppAction: Equatable {
case factAlertDismissed
case decrementButtonTapped
case incrementButtonTapped
case numberFactButtonTapped
case numberFactResponse(TaskResult<String>)
}
Next we model the environment of dependencies this feature needs to do its job. In particular, to fetch a number fact we can model an async throwing function from Int
to String
:
struct AppEnvironment {
var numberFact: (Int) async throws -> String
}
Em seguida, implementamos um Reducer que implementa a lógica para este domínio. Ele descreve como alterar o estado atual para o próximo estado e descreve quais efeitos precisam ser executados. Algumas ações não precisam executar efeitos e podem retornar .none
para representar isso:
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment in
switch action {
case .factAlertDismissed:
state.numberFactAlert = nil
return .none
case .decrementButtonTapped:
state.count -= 1
return .none
case .incrementButtonTapped:
state.count += 1
return .none
case .numberFactButtonTapped:
return .task {
await .numberFactResponse(TaskResult { try await environment.numberFact(state.count) })
}
case let .numberFactResponse(.success(fact)):
state.numberFactAlert = fact
return .none
case .numberFactResponse(.failure):
state.numberFactAlert = "Could not load a number fact :("
return .none
}
}
E, finalmente, definimos a visualização que exibe a feature. Ele mantém um Store<AppState, AppAction>
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 Store para que o estado mude. Também devemos introduzir um struct wrapper em torno do alerta de fato para torná-lo Identifiable
, que o modificador de visão .alert
requer:
struct AppView: View {
let store: Store<AppState, AppAction>
var body: some View {
WithViewStore(self.store) { viewStore in
VStack {
HStack {
Button("−") { viewStore.send(.decrementButtonTapped) }
Text("\(viewStore.count)")
Button("+") { viewStore.send(.incrementButtonTapped) }
}
Button("Number fact") { viewStore.send(.numberFactButtonTapped) }
}
.alert(
item: viewStore.binding(
get: { $0.numberFactAlert.map(FactAlert.init(title:)) },
send: .factAlertDismissed
),
content: { Alert(title: Text($0.title)) }
)
}
}
}
struct FactAlert: Identifiable {
var title: String
var id: String { self.title }
}
É importante observar que conseguimos implementar todo esse recurso sem ter um efeito real e ao vivo disponível. Isso é importante porque significa que os recursos podem ser construídos isoladamente sem construir suas dependências, o que pode ajudar nos tempos de compilação.
Também é simples ter um controlador UIKit acionado a partir desta Store. Você se inscreve na loja em viewDidLoad
para atualizar a UI e mostrar alertas. O código é um pouco mais longo que a versão SwiftUI, então nós o recolhemos aqui:
Clique para expandir!
class AppViewController: UIViewController {
let viewStore: ViewStore<AppState, AppAction>
var cancellables: Set<AnyCancellable> = []
init(store: Store<AppState, AppAction>) {
self.viewStore = ViewStore(store)
super.init(nibName: nil, bundle: nil)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad() {
super.viewDidLoad()
let countLabel = UILabel()
let incrementButton = UIButton()
let decrementButton = UIButton()
let factButton = UIButton()
// Omitted: Add subviews and set up constraints...
self.viewStore.publisher
.map { "\($0.count)" }
.assign(to: \.text, on: countLabel)
.store(in: &self.cancellables)
self.viewStore.publisher.numberFactAlert
.sink { [weak self] numberFactAlert in
let alertController = UIAlertController(
title: numberFactAlert, message: nil, preferredStyle: .alert
)
alertController.addAction(
UIAlertAction(
title: "Ok",
style: .default,
handler: { _ in self?.viewStore.send(.factAlertDismissed) }
)
)
self?.present(alertController, animated: true, completion: nil)
}
.store(in: &self.cancellables)
}
@objc private func incrementButtonTapped() {
self.viewStore.send(.incrementButtonTapped)
}
@objc private func decrementButtonTapped() {
self.viewStore.send(.decrementButtonTapped)
}
@objc private func factButtonTapped() {
self.viewStore.send(.numberFactButtonTapped)
}
}
Quando estivermos prontos para exibir essa visualização, por exemplo, no ponto de entrada do aplicativo, podemos construir uma Store. Este é o momento em que precisamos fornecer as dependências, incluindo o endpoint numberFact
que está obtendo as informações do mundo real:
@main
struct CaseStudiesApp: App {
var body: some Scene {
AppView(
store: Store(
initialState: AppState(),
reducer: appReducer,
environment: AppEnvironment(
numberFact: { number in
let (data, _) = try await URLSession.shared
.data(from: .init(string: "http://numbersapi.com/\(number)")!)
return String(decoding: data, using: UTF8.self)
}
)
)
)
}
E isso é suficiente para colocar algo na tela para brincar. Definitivamente, são mais algumas etapas do que se você fizesse isso no SwiftUI vanilla, mas há alguns benefícios. Ele nos dá uma maneira consistente de aplicar mutações de estado, em vez de espalhar a lógica em alguns observable objects e em vários closures 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 muito trabalho adicional.
Para testar, você primeiro cria um TestStore
com as mesmas informações que você usaria para criar um Store
normal, exceto que desta vez podemos fornecer dependências amigáveis para testes. Em particular, agora podemos usar uma implementação numberFact
que retorna imediatamente um valor que controlamos em vez de esperar do mundo real:
@MainActor
func testFeature() async {
let store = TestStore(
initialState: AppState(),
reducer: appReducer,
environment: AppEnvironment(
numberFact: { "\($0) is a good number Brent" }
)
)
}
Uma vez que o teste da Store é criado, podemos usá-lo para fazer uma comprovação de todo um fluxo de etapas do usuário. A cada passo do caminho precisamos provar que o estado mudou como esperamos. Além disso, se uma etapa fizer com que um efeito seja executado, o que alimenta os dados de volta a Store, devemos comprovar que essas ações foram recebidas corretamente.
O próximo teste faz com que o usuário aumente e diminua a contagem, depois pergunta sobre a curiosidade daquele número, e a resposta daquele efeito aciona um alerta a ser exibido, e por fim, dispensando o alerta desaparece.
// Teste se tocar nos botões de incremento/decremento altera a contagem
await store.send(.incrementButtonTapped) {
$0.count = 1
}
await store.send(.decrementButtonTapped) {
$0.count = 0
}
// Teste se tocar no botão de fato nos faz receber uma resposta do efeito. Observe
// que temos que aguardar o recebimento porque o efeito é assíncrono e, portanto, demora um pouco para ser emitido.
await store.send(.numberFactButtonTapped)
await store.receive(.numberFactResponse(.success("0 is a good number Brent"))) {
$0.numberFactAlert = "0 is a good number Brent"
}
// E finalmente descarte o alerta
await store.send(.factAlertDismissed) {
$0.numberFactAlert = nil
}
Esse é o básico da construção e teste de uma feature 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 e ver usos mais avançados.
A Composable Architecture vem com várias ferramentas para auxiliar na depuração.
-
reducer.debug()
imprime na tela cada ação que o Reducer recebe e cada mutação feita no estado.received action: AppAction.todoCheckboxTapped(id: UUID(5834811A-83B4-4E5E-BCD3-8A38F6BDCA90)) AppState( todos: [ Todo( - isComplete: false, + isComplete: true, description: "Milk", id: 5834811A-83B4-4E5E-BCD3-8A38F6BDCA90 ), … (2 unchanged) ] )
-
reducer.signpost()
implementa um Reducer com sinais para obter informações sobre quanto tempo as ações levam para serem executadas e quando os efeitos são executados.
Um dos princípios mais importantes da Composable Architecture é que os efeitos colaterais nunca são executados diretamente, mas sim agrupados no tipo Effect
, retornados dos Reducers, e então o Store
executa o efeito posteriormente. Isso é crucial para simplificar como os dados fluem por meio de um aplicativo e para obter testabilidade em todo o ciclo de ponta a ponta da ação do usuário para efetuar a execução.
No entanto, isso também significa que muitas bibliotecas e SDKs com os quais você interage diariamente precisam ser adaptados para serem um pouco mais amigáveis ao estilo Composable Architecture. É por isso que gostaríamos de aliviar a dor de usar alguns dos frameworks mais populares da Apple, fornecendo bibliotecas de wrapper que expõem sua funcionalidade de uma maneira que funcione bem com nossa biblioteca. Até agora apoiamos:
ComposableCoreLocation
: Um wrapper em torno doCLLocationManager
que facilita o uso a partir de um Reducer e fácil de escrever testes de como sua lógica interage com a funcionalidade doCLLocationManager
.ComposableCoreMotion
: Um wrapper em torno doCMMotionManager
que facilita o uso a partir de um Reducer e fácil de escrever testes de como sua lógica interage com a funcionalidade doCMMotionManager
.- Mais para vir em breve. Fique de olho 😉
Se você estiver interessado em contribuir com uma biblioteca wrapper para um framework que ainda não abordamos, sinta-se à vontade para abrir um issue expressando seu interesse para que possamos discutir um caminho a seguir.
-
Como a Composable Architecture se compara ao Elm, Redux e outros?
Expanda para ver a resposta
A Composable Architecture (TCA) é construída sobre uma base de ideias popularizadas pela Elm Architecture (TEA) e Redux, mas feita para se sentir em casa na linguagem Swift e nas plataformas da Apple.De certa forma, o TCA é um pouco mais opinativo do que as outras bibliotecas. Por exemplo, o Redux não é prescritivo com a forma como se executam os efeitos secundários, mas o TCA exige que todos os efeitos secundários sejam modelados no tipo
Effect
e retornados do reducer.De outras maneiras, o TCA é um pouco mais simples do que as outras bibliotecas. Por exemplo, Elm controla quais tipos de efeitos podem ser criados através do tipo
Cmd
, mas o TCA permite uma saída de escape para qualquer tipo de efeito desde queEffect
esteja em conformidade com o protocolo CombinePublisher
.E há certas coisas que o TCA prioriza altamente que não são pontos de foco para Redux, Elm ou a maioria das outras bibliotecas. Por exemplo, a composição é um aspecto muito importante do TCA, que é o processo de quebrar grandes features em unidades menores que podem ser coladas. Isso é feito com os operadores
pullback
ecombine
em reducers, e ajuda a lidar com recursos complexos, bem como a modularização para uma base de código melhor isolada e tempos de compilação aprimorados.
A Composable Architecture depende da estrutura Combine, portanto, o deployment targets mínimo necessário é o iOS 13, macOS 10.15, Mac Catalyst 13, tvOS 13 e watchOS 6. Se seu aplicativo deve suportar sistemas operacionais mais antigos, há forks para ReactiveSwift e RxSwift que você pode adotar!
Você pode adicionar ComposableArchitecture a um projeto Xcode adicionando-o como uma dependência de pacote.
- No menu File, selecione Add Packages...
- Digite "https://github.com/pointfreeco/swift-composable-architecture" no campo de texto URL do repositório de pacotes
- Dependendo de como seu projeto está estruturado:
- Se você tiver um único target que precisa de acesso à biblioteca, adicione ComposableArchitecture diretamente ao seu aplicativo.
- Se você quiser usar essa biblioteca em vários Xcode targets ou misturar Xcode targets e SPM targets, deverá criar uma estrutura compartilhada que dependa de ComposableArchitecture e, em seguida, dependa dessa estrutura em todos os seus targets. Para um exemplo disso, confira o aplicativo de demonstração Tic-Tac-Toe, já que nele muitos recursos são divididos em módulos e consomem a biblioteca estática dessa forma no pacote Swift tic-tac-toe.
A documentação para releases e main
estão disponíveis aqui:
Outras versões
- 0.38.0
- 0.37.0
- 0.36.0
- 0.35.0
- 0.34.0
- 0.33.1
- 0.33.0
- 0.32.0
- 0.31.0
- 0.30.0
- 0.29.0
- 0.28.1
- 0.28.0
- 0.27.1
- 0.27.0
- 0.26.0
- 0.25.1
- 0.25.0
- 0.24.0
- 0.23.0
- 0.22.0
- 0.21.0
- 0.20.0
- 0.19.0
- 0.18.0
- 0.17.0
- 0.16.0
- 0.15.0
- 0.14.0
- 0.13.0
- 0.12.0
- 0.11.0
- 0.10.0
- 0.9.0
- 0.8.0
- 0.7.0
- 0.6.0
- 0.5.0
- 0.4.0
- 0.3.0
- 0.2.0
- 0.1.5
- 0.1.4
- 0.1.3
- 0.1.2
- 0.1.1
- 0.1.0
Se você quiser discutir a Composable Architecture ou tiver alguma dúvida sobre como usá-la para resolver um problema específico, você pode iniciar um tópico nas discussões deste repositório, ou pergunte no fórum Swift.
As seguintes traduções deste README foram contribuídas por membros da comunidade:
Se você quiser contribuir com uma tradução, por favor abra um PR com um link para um Gist!
As seguintes pessoas deram feedback sobre a biblioteca em seus estágios iniciais e ajudaram a tornar a biblioteca o que ela é 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, and all of the Point-Free subscribers 😁.
Agradecimentos especiais a Chris Liscio que nos ajudou a trabalhar com muitas peculiaridades estranhas do SwiftUI e ajudou a refinar a API final.
E graças ao projeto Shai Mishali e CombineCommunity, do qual tiramos a implementação de Publishers.Create
, que usamos em Effect
para ajudar a conectar APIs baseadas em delegação e retorno de chamada, facilitando muito 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.
Há também muitas bibliotecas de arquitetura na comunidade Swift e iOS. Cada um deles tem seu próprio conjunto de prioridades e trade-offs que diferem da Composable Architecture.
-
And more
Esta biblioteca é lançada sob a licença do MIT. Consulte LICENSE para obter detalhes.