- Notifications
You must be signed in to change notification settings - Fork0
A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
License
Trilda-Architectures/pointfreeco-swift-composable-architecture
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
The Composable Architecture (TCA, for short) is a library for building applications in a consistentand understandable way, with composition, testing, and ergonomics in mind. It can be used inSwiftUI, UIKit, and more, and on any Apple platform (iOS, macOS, iPadOS, visionOS, tvOS, and watchOS).
- What is the Composable Architecture?
- Learn more
- Examples
- Basic usage
- Documentation
- FAQ
- Community
- Installation
- Translations
This library provides a few core tools that can be used to build applications of varying purpose andcomplexity. It provides compelling stories that you can follow to solve many problems you encounterday-to-day when building applications, such as:
State management
How to manage the state of your application using simple value types, and share state acrossmany screens so that mutations in one screen can be immediately observed in another screen.Composition
How to break down large features into smaller components that can be extracted to their own,isolated modules and be easily glued back together to form the feature.Side effects
How to let certain parts of the application talk to the outside world in the most testableand understandable way possible.Testing
How to not only test a feature built in the architecture, but also write integration testsfor features that have been composed of many parts, and write end-to-end tests to understand howside effects influence your application. This allows you to make strong guarantees that yourbusiness logic is running in the way you expect.Ergonomics
How to accomplish all of the above in a simple API with as few concepts and moving parts aspossible.
The Composable Architecture was designed over the course of many episodes onPoint-Free, a video series exploring advanced programming topics in the Swift language,hosted byBrandon Williams andStephen Celis.
You can watch all of the episodeshere, as well as a dedicated,multiparttour of the architecture from scratch.

This repo comes withlots of examples to demonstrate how to solve common and complex problems withthe Composable Architecture. Check outthis directory to see them all, including:
- Case Studies
- Getting started
- Effects
- Navigation
- Higher-order reducers
- Reusable components
- Location manager
- Motion manager
- Search
- Speech Recognition
- SyncUps app
- Tic-Tac-Toe
- Todos
- Voice memos
Looking for something more substantial? Check out the source code forisowords, aniOS word search game built in SwiftUI and the Composable Architecture.
Note
For a step-by-step interactive tutorial, be sure to check outMeet the ComposableArchitecture.
To build a feature using the Composable Architecture you define some types and values that modelyour domain:
- State: A type that describes the data your feature needs to perform its logic and render itsUI.
- Action: A type that represents all of the actions that can happen in your feature, such asuser actions, notifications, event sources and more.
- Reducer: A function that describes how to evolve the current state of the app to the nextstate given an action. The reducer is also responsible for returning any effects that should berun, such as API requests, which can be done by returning an
Effectvalue. - Store: The runtime that actually drives your feature. You send all user actions to the storeso that the store can run the reducer and effects, and you can observe state changes in the storeso that you can update UI.
The benefits of doing this are that you will instantly unlock testability of your feature, and youwill be able to break large, complex features into smaller domains that can be glued together.
As a basic example, consider a UI that shows a number along with "+" and "−" buttons that incrementand decrement the number. To make things interesting, suppose there is also a button that whentapped makes an API request to fetch a random fact about that number and displays it in the view.
To implement this feature we create a new type that will house the domain and behavior of thefeature, and it will be annotated with the@Reducer macro:
import ComposableArchitecture@ReducerstructFeature{}
In here we need to define a type for the feature's state, which consists of an integer for thecurrent count, as well as an optional string that represents the fact being presented:
@ReducerstructFeature{@ObservableStatestructState:Equatable{varcount=0varnumberFact:String?}}
Note
We've applied the@ObservableState macro toState in order to take advantage of theobservation tools in the library.
We also need to define a type for the feature's actions. There are the obvious actions, such astapping the decrement button, increment button, or fact button. But there are also some slightlynon-obvious ones, such as the action that occurs when we receive a response from the fact APIrequest:
@ReducerstructFeature{@ObservableStatestructState:Equatable{ /* ... */}enumAction{case decrementButtonTappedcase incrementButtonTappedcase numberFactButtonTappedcase numberFactResponse(String)}}
And then we implement thebody property, which is responsible for composing the actual logic andbehavior for the feature. In it we can use theReduce reducer to describe how to change thecurrent state to the next state, and what effects need to be executed. Some actions don't need toexecute effects, and they can return.none to represent that:
@ReducerstructFeature{@ObservableStatestructState:Equatable{ /* ... */}enumAction{ /* ... */}varbody:someReducer<State,Action>{Reduce{ state, actioninswitch action{case.decrementButtonTapped: state.count-=1return.nonecase.incrementButtonTapped: state.count+=1return.nonecase.numberFactButtonTapped:return.run{[count= state.count] sendinlet(data, _)=tryawaitURLSession.shared.data( from:URL(string:"http://numbersapi.com/\(count)/trivia")!)awaitsend(.numberFactResponse(String(decoding: data, as:UTF8.self)))}caselet.numberFactResponse(fact): state.numberFact= factreturn.none}}}}
And then finally we define the view that displays the feature. It holds onto aStoreOf<Feature>so that it can observe all changes to the state and re-render, and we can send all user actions tothe store so that state changes:
structFeatureView:View{letstore:StoreOf<Feature>varbody:someView{Form{Section{Text("\(store.count)")Button("Decrement"){ store.send(.decrementButtonTapped)}Button("Increment"){ store.send(.incrementButtonTapped)}}Section{Button("Number fact"){ store.send(.numberFactButtonTapped)}}iflet fact= store.numberFact{Text(fact)}}}}
It is also straightforward to have a UIKit controller driven off of this store. You can observestate changes in the store inviewDidLoad, and then populate the UI components with data fromthe store. The code is a bit longer than the SwiftUI version, so we have collapsed it here:
Click to expand!
classFeatureViewController:UIViewController{letstore:StoreOf<Feature>init(store:StoreOf<Feature>){self.store= store super.init(nibName:nil, bundle:nil)}requiredinit?(coder:NSCoder){fatalError("init(coder:) has not been implemented")}overridefunc viewDidLoad(){ super.viewDidLoad()letcountLabel=UILabel()letdecrementButton=UIButton()letincrementButton=UIButton()letfactLabel=UILabel() // Omitted: Add subviews and set up constraints...observe{[weak self]inguardlet selfelse{return} countLabel.text="\(self.store.count)" factLabel.text=self.store.numberFact}}@objcprivatefunc incrementButtonTapped(){self.store.send(.incrementButtonTapped)}@objcprivatefunc decrementButtonTapped(){self.store.send(.decrementButtonTapped)}@objcprivatefunc factButtonTapped(){self.store.send(.numberFactButtonTapped)}}
Once we are ready to display this view, for example in the app's entry point, we can construct astore. This can be done by specifying the initial state to start the application in, as well asthe reducer that will power the application:
import ComposableArchitecture@mainstructMyApp:App{varbody:someScene{WindowGroup{FeatureView( store:Store(initialState:Feature.State()){Feature()})}}}
And that is enough to get something on the screen to play around with. It's definitely a few moresteps than if you were to do this in a vanilla SwiftUI way, but there are a few benefits. It givesus a consistent manner to apply state mutations, instead of scattering logic in some observableobjects and in various action closures of UI components. It also gives us a concise way ofexpressing side effects. And we can immediately test this logic, including the effects, withoutdoing much additional work.
Note
For more in-depth information on testing, see the dedicatedtesting article.
To test use aTestStore, which can be created with the same information as theStore, but itdoes extra work to allow you to assert how your feature evolves as actions are sent:
@Testfunc basics()async{letstore=TestStore(initialState:Feature.State()){Feature()}}
Once the test store is created we can use it to make an assertion of an entire user flow of steps.Each step of the way we need to prove that state changed how we expect. For example, we cansimulate the user flow of tapping on the increment and decrement buttons:
// Test that tapping on the increment/decrement buttons changes the countawait store.send(.incrementButtonTapped){ $0.count=1}await store.send(.decrementButtonTapped){ $0.count=0}
Further, if a step causes an effect to be executed, which feeds data back into the store, we mustassert on that. For example, if we simulate the user tapping on the fact button we expect toreceive a fact response back with the fact, which then causes thenumberFact state to bepopulated:
await store.send(.numberFactButtonTapped)await store.receive(\.numberFactResponse){ $0.numberFact=???}
However, how do we know what fact is going to be sent back to us?
Currently our reducer is using an effect that reaches out into the real world to hit an API server,and that means we have no way to control its behavior. We are at the whims of our internetconnectivity and the availability of the API server in order to write this test.
It would be better for this dependency to be passed to the reducer so that we can use a livedependency when running the application on a device, but use a mocked dependency for tests. We cando this by adding a property to theFeature reducer:
@ReducerstructFeature{letnumberFact:(Int)asyncthrows->String // ...}
Then we can use it in thereduce implementation:
case.numberFactButtonTapped: return.run{[count= state.count] sendinletfact=tryawaitself.numberFact(count)awaitsend(.numberFactResponse(fact))}
And in the entry point of the application we can provide a version of the dependency that actuallyinteracts with the real world API server:
@mainstructMyApp:App{varbody:someScene{WindowGroup{FeatureView( store:Store(initialState:Feature.State()){Feature( numberFact:{ numberinlet(data, _)=tryawaitURLSession.shared.data( from:URL(string:"http://numbersapi.com/\(number)")!)returnString(decoding: data, as:UTF8.self)})})}}}
But in tests we can use a mock dependency that immediately returns a deterministic, predictablefact:
@Testfunc basics()async{letstore=TestStore(initialState:Feature.State()){Feature(numberFact:{"\($0) is a good number Brent"})}}
With that little bit of upfront work we can finish the test by simulating the user tapping on thefact button, and then receiving the response from the dependency to present the fact:
await store.send(.numberFactButtonTapped)await store.receive(\.numberFactResponse){ $0.numberFact="0 is a good number Brent"}
We can also improve the ergonomics of using thenumberFact dependency in our application. Overtime the application may evolve into many features, and some of those features may also want accesstonumberFact, and explicitly passing it through all layers can get annoying. There is a processyou can follow to “register” dependencies with the library, making them instantly available to anylayer in the application.
Note
For more in-depth information on dependency management, see the dedicateddependencies article.
We can start by wrapping the number fact functionality in a new type:
structNumberFactClient{varfetch:(Int)asyncthrows->String}
And then registering that type with the dependency management system by conforming the client totheDependencyKey protocol, which requires you to specify the live value to use when running theapplication in simulators or devices:
extensionNumberFactClient:DependencyKey{staticletliveValue=Self( fetch:{ numberinlet(data, _)=tryawaitURLSession.shared.data(from:URL(string:"http://numbersapi.com/\(number)")!)returnString(decoding: data, as:UTF8.self)})}extensionDependencyValues{varnumberFact:NumberFactClient{get{self[NumberFactClient.self]}set{self[NumberFactClient.self]= newValue}}}
With that little bit of upfront work done you can instantly start making use of the dependency inany feature by using the@Dependency property wrapper:
@Reducer struct Feature {- let numberFact: (Int) async throws -> String+ @Dependency(\.numberFact) var numberFact …- try await self.numberFact(count)+ try await self.numberFact.fetch(count) }This code works exactly as it did before, but you no longer have to explicitly pass the dependencywhen constructing the feature's reducer. When running the app in previews, the simulator or on adevice, the live dependency will be provided to the reducer, and in tests the test dependency willbe provided.
This means the entry point to the application no longer needs to construct dependencies:
@mainstructMyApp:App{varbody:someScene{WindowGroup{FeatureView( store:Store(initialState:Feature.State()){Feature()})}}}
And the test store can be constructed without specifying any dependencies, but you can stilloverride any dependency you need to for the purpose of the test:
letstore=TestStore(initialState:Feature.State()){Feature()} withDependencies:{ $0.numberFact.fetch={"\($0) is a good number Brent"}}// ...
That is the basics of building and testing a feature in the Composable Architecture. There area lot more things to be explored, such as composition, modularity, adaptability, and complexeffects. TheExamples directory has a bunch of projects to explore to see moreadvanced usages.
The documentation for releases andmain are available here:
Other versions
- 1.16.0 (migration guide)
- 1.15.0 (migration guide)
- 1.14.0 (migration guide)
- 1.13.0 (migration guide)
- 1.12.0 (migration guide)
- 1.11.0 (migration guide)
- 1.10.0 (migration guide)
- 1.9.0 (migration guide)
- 1.8.0 (migration guide)
- 1.7.0 (migration guide)
- 1.6.0 (migration guide)
- 1.5.0 (migration guide)
- 1.4.0 (migration guide)
- 1.3.0
- 1.2.0
- 1.1.0
- 1.0.0
- 0.59.0
- 0.58.0
- 0.57.0
There are a number of articles in the documentation that you may find helpful as you become morecomfortable with the library:
We have adedicated article for all of the most frequently asked questions andcomments people have concerning the library.
If you want to discuss the Composable Architecture or have a question about how to use it to solvea particular problem, there are a number of places you can discuss with fellowPoint-Free enthusiasts:
- For long-form discussions, we recommend thediscussions tab of this repo.
- For casual chat, we recommend thePoint-Free Community slack.
You can add ComposableArchitecture to an Xcode project by adding it as a package dependency.
- From theFile menu, selectAdd Package Dependencies...
- Enter "https://github.com/pointfreeco/swift-composable-architecture" into the packagerepository URL text field
- Depending on how your project is structured:
- If you have a single application target that needs access to the library, then addComposableArchitecture directly to your application.
- If you want to use this library from multiple Xcode targets, or mix Xcode targets and SPMtargets, you must create a shared framework that depends onComposableArchitecture andthen depend on that framework in all of your targets. For an example of this, check out theTic-Tac-Toe demo application, which splits lots of features intomodules and consumes the static library in this fashion using thetic-tac-toe Swiftpackage.
The Composable Architecture is built with extensibility in mind, and there are a number ofcommunity-supported libraries available to enhance your applications:
- Composable Architecture Extras:A companion library to the Composable Architecture.
- TCAComposer: A macro framework for generatingboiler-plate code in the Composable Architecture.
- TCACoordinators: The coordinator patternin the Composable Architecture.
If you'd like to contribute a library, pleaseopen aPR with a linkto it!
The following translations of this README have been contributed by members of the community:
- Arabic
- French
- Hindi
- Indonesian
- Italian
- Japanese
- Korean
- Polish
- Portuguese
- Russian
- Simplified Chinese
- Spanish
- Turkish
- Ukrainian
If you'd like to contribute a translation, pleaseopen aPR with a linkto aGist!
The following people gave feedback on the library at its early stages and helped make the librarywhat it is today:
Paul Colton, Kaan Dedeoglu, Matt Diephouse, Josef Doležal, Eimantas, Matthew Johnson, GeorgeKaimakas, Nikita Leonov, Christopher Liscio, Jeffrey Macko, Alejandro Martinez, Shai Mishali, WillisPlummer, Simon-Pierre Roy, Justin Price, Sven A. Schmidt, Kyle Sherman, Petr Šíma, Jasdev Singh,Maxim Smirnov, Ryan Stone, Daniel Hollis Tavares, and all of thePoint-Freesubscribers 😁.
Special thanks toChris Liscio who helped us work through many strangeSwiftUI quirks and helped refine the final API.
And thanks toShai Mishali and theCombineCommunity project, from which we tooktheir implementation ofPublishers.Create, which we use inEffect to help bridge delegate andcallback-based APIs, making it much easier to interface with 3rd party frameworks.
The Composable Architecture was built on a foundation of ideas started by other libraries, inparticularElm andRedux.
There are also many architecture libraries in the Swift and iOS community. Each one of these hastheir own set of priorities and trade-offs that differ from the Composable Architecture.
This library is released under the MIT license. SeeLICENSE for details.
About
A library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.
Resources
License
Code of conduct
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Packages0
Languages
- Swift99.9%
- Makefile0.1%
