Files
swift-composable-architectu…/Sources/ComposableArchitecture/Documentation.docc/Articles/GettingStarted.md
Stephen Celis 57e804f1cc Macro bonanza (#2553)
* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Silence test warnings

* wip

* wip

* wip

* update a bunch of docs

* wip

* wip

* fix

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Kill integration tests for now

* wip

* wip

* wip

* wip

* updating docs for @Reducer macro

* replaced more Reducer protocols with @Reducer

* Fixed some broken docc references

* wip

* Some @Reducer docs

* more docs

* convert some old styles to new style

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* bump

* update tutorials to use body

* update tutorials to use DML on destination state enum

* Add diagnostic

* wip

* updated a few more tests

* wip

* wip

* Add another gotcha

* wip

* wip

* wip

* fixes

* wip

* wip

* wip

* wip

* wip

* fix

* wip

* remove for now

* wip

* wip

* updated some docs

* migration guides

* more migration guide

* fix ci

* fix

* soft deprecate all apis using AnyCasePath

* wip

* Fix

* fix tests

* swift-format 509 compatibility

* wip

* wip

* Update Sources/ComposableArchitecture/Macros.swift

Co-authored-by: Mateusz Bąk <bakmatthew@icloud.com>

* wip

* wip

* update optional state case study

* remove initializer

* Don't use @State for BasicsView integration demo

* fix tests

* remove reduce diagnostics for now

* diagnose error not warning

* Update Sources/ComposableArchitecture/Macros.swift

Co-authored-by: Jesse Tipton <jesse@jessetipton.com>

* wip

* move integration tests to cron

* Revert "move integration tests to cron"

This reverts commit f9bdf2f04b.

* disable flakey tests on CI

* wip

* wip

* Revert "Revert "move integration tests to cron""

This reverts commit 66aafa7327.

* fix

* wip

* fix

---------

Co-authored-by: Brandon Williams <mbrandonw@hey.com>
Co-authored-by: Mateusz Bąk <bakmatthew@icloud.com>
Co-authored-by: Brandon Williams <135203+mbrandonw@users.noreply.github.com>
Co-authored-by: Jesse Tipton <jesse@jessetipton.com>
2023-11-13 12:57:35 -08:00

479 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Getting started
Learn how to integrate the Composable Architecture into your project and write your first
application.
## Adding the Composable Architecture as a dependency
To use the Composable Architecture in a SwiftPM project, add it to the dependencies of your
Package.swift and specify the `ComposableArchitecture` product in any targets that need access to
the library:
```swift
let package = Package(
dependencies: [
.package(
url: "https://github.com/pointfreeco/swift-composable-architecture",
from: "1.0.0"
),
],
targets: [
.target(
name: "<target-name>",
dependencies: [
.product(
name: "ComposableArchitecture",
package: "swift-composable-architecture"
)
]
)
]
)
```
## Writing your first feature
To build a feature using the Composable Architecture you define some types and values that model
your domain:
* **State**: A type that describes the data your feature needs to perform its logic and render its
UI.
* **Action**: A type that represents all of the actions that can happen in your feature, such as
user actions, notifications, event sources and more.
* **Reducer**: A function that describes how to evolve the current state of the app to the next
state given an action. The reducer is also responsible for returning any effects that should be
run, such as API requests, which can be done by returning an `Effect` value.
* **Store**: The runtime that actually drives your feature. You send all user actions to the store
so that the store can run the reducer and effects, and you can observe state changes in the
store so that you can update UI.
The benefits of doing this are that you will instantly unlock testability of your feature, and you
will 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 increment
and decrement the number. To make things interesting, suppose there is also a button that when
tapped makes an API request to fetch a random fact about that number and then displays the fact in
an alert.
To implement this feature we create a new type that will house the domain and behavior of the
feature, and you will annotate the type with the ``Reducer()`` macro:
```swift
@Reducer
struct Feature {
}
```
In here we need to define a type for the feature's state, which consists of an integer for the
current count, as well as an optional string that represents the title of the alert we want to show
(optional because `nil` represents not showing an alert):
```swift
@Reducer
struct Feature {
struct State: Equatable {
var count = 0
var numberFactAlert: String?
}
}
```
We also need to define a type for the feature's actions. There are the obvious actions, such as
tapping the decrement button, increment button, or fact button. But there are also some slightly
non-obvious ones, such as the action of the user dismissing the alert, and the action that occurs
when we receive a response from the fact API request:
```swift
@Reducer
struct Feature {
struct State: Equatable { /* ... */ }
enum Action {
case factAlertDismissed
case decrementButtonTapped
case incrementButtonTapped
case numberFactButtonTapped
case numberFactResponse(String)
}
}
```
And then we implement the ``Reducer/body-swift.property`` property, which is responsible for
handling the actual logic and behavior for the feature. In it we can use the ``Reduce`` reducer to
describe how to change the current state to the next state, and what effects need to be executed.
Some actions don't need to execute effects, and they can return ``Effect/none`` to represent that:
```swift
@Reducer
struct Feature {
struct State: Equatable { /* ... */ }
enum Action { /* ... */ }
var body: some Reducer<State, Action> {
Reduce { state, action 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 .run { [count = state.count] send in
let (data, _) = try await URLSession.shared.data(
from: URL(string: "http://numbersapi.com/\(count)/trivia")!
)
await send(
.numberFactResponse(String(decoding: data, as: UTF8.self))
)
}
case let .numberFactResponse(fact):
state.numberFactAlert = fact
return .none
}
}
}
}
```
And then finally we define the view that displays the feature. It holds onto a `StoreOf<Feature>`
so that it can observe all changes to the state and re-render, and we can send all user actions to
the store so that state changes. We must also introduce a struct wrapper around the fact alert to
make it `Identifiable`, which the `.alert` view modifier requires:
```swift
struct FeatureView: View {
let store: StoreOf<Feature>
var body: some View {
WithViewStore(self.store, observe: { $0 }) { 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 }
}
```
It is also straightforward to have a UIKit controller driven off of this store. You subscribe to the
store in `viewDidLoad` in order to update the UI and show alerts. The code is a bit longer than the
SwiftUI version:
```swift
class FeatureViewController: UIViewController {
let viewStore: ViewStoreOf<Feature>
var cancellables: Set<AnyCancellable> = []
init(store: StoreOf<Feature>) {
self.viewStore = ViewStore(store, observe: { $0 })
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)
}
}
```
Once we are ready to display this view, for example in the app's entry point, we can construct a
store. This can be done by specifying the initial state to start the application in, as well as the
reducer that will power the application:
```swift
@main
struct MyApp: App {
var body: some Scene {
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 more
steps than if you were to do this in a vanilla SwiftUI way, but there are a few benefits. It gives
us a consistent manner to apply state mutations, instead of scattering logic in some observable
objects and in various action closures of UI components. It also gives us a concise way of
expressing side effects. And we can immediately test this logic, including the effects, without
doing much additional work.
## Testing your feature
To test use a ``TestStore``, which can be created with the same information as the ``Store``, but it
does extra work to allow you to assert how your feature evolves as actions are sent:
```swift
@MainActor
func testFeature() async {
let store = 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 can simulate
the user flow of tapping on the increment and decrement buttons:
```swift
// Test that tapping on the increment/decrement buttons changes the count
await 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 must
assert on that. For example, if we simulate the user tapping on the fact button we expect to
receive a fact response back with the fact, which then causes the alert to show:
```swift
await store.send(.numberFactButtonTapped)
await store.receive(\.numberFactResponse) {
$0.numberFactAlert = "???"
}
```
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 internet
connectivity 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 live
dependency when running the application on a device, but use a mocked dependency for tests. We
can do this by adding a property to the `Feature` reducer:
```swift
@Reducer
struct Feature {
let numberFact: (Int) async throws -> String
// ...
}
```
Then we can use it in the `reduce` implementation:
```swift
case .numberFactButtonTapped:
return .run { [count = state.count] send in
let fact = try await self.numberFact(count)
await send(.numberFactResponse(fact))
}
```
And in the entry point of the application we can provide a version of the dependency that actually
interacts with the real world API server:
```swift
@main
struct MyApp: App {
var body: some Scene {
FeatureView(
store: Store(initialState: Feature.State()) {
Feature(
numberFact: { number in
let (data, _) = try await URLSession.shared.data(
from: .init(string: "http://numbersapi.com/\(number)")!
)
return String(decoding: data, as: UTF8.self)
}
)
}
)
}
}
```
But in tests we can use a mock dependency that immediately returns a deterministic, predictable fact:
```swift
@MainActor
func testFeature() async {
let store = 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 the
fact button, receiving the response from the dependency to trigger the alert, and then dismissing
the alert:
```swift
await store.send(.numberFactButtonTapped)
await store.receive(\.numberFactResponse) {
$0.numberFactAlert = "0 is a good number Brent"
}
await store.send(.factAlertDismissed) {
$0.numberFactAlert = nil
}
```
We can also improve the ergonomics of using the `numberFact` dependency in our application. Over
time the application may evolve into many features, and some of those features may also want access
to `numberFact`, and explicitly passing it through all layers can get annoying. There is a process
you can follow to “register” dependencies with the library, making them instantly available to any
layer in the application.
We can start by wrapping the number fact functionality in a new type:
```swift
struct NumberFactClient {
var fetch: (Int) async throws -> String
}
```
And then registering that type with the dependency management system, which is quite similar to
how SwiftUI's environment values works, except you specify the live implementation of the
dependency to be used by default:
```swift
private enum NumberFactClientKey: DependencyKey {
static let liveValue = NumberFactClient(
fetch: { number in
let (data, _) = try await URLSession.shared.data(
from: .init(string: "http://numbersapi.com/\(number)")!
)
return String(decoding: data, as: UTF8.self)
}
)
}
extension DependencyValues {
var numberFact: NumberFactClient {
get { self[NumberFactClientKey.self] }
set { self[NumberFactClientKey.self] = newValue }
}
}
```
With that little bit of upfront work done you can instantly start making use of the dependency in
any feature:
```swift
@Reducer
struct Feature {
struct State { /* ... */ }
enum Action { /* ... */ }
@Dependency(\.numberFact) var numberFact
// ...
}
```
This code works exactly as it did before, but you no longer have to explicitly pass the dependency
when constructing the feature's reducer. When running the app in previews, the simulator or on a
device, the live dependency will be provided to the reducer, and in tests the test dependency will
be provided.
This means the entry point to the application no longer needs to construct dependencies:
```swift
@main
struct MyApp: App {
var body: some Scene {
FeatureView(
store: Store(initialState: Feature.State()) {
Feature()
}
)
}
}
```
And the test store can be constructed without specifying any dependencies, but you can still
override any dependency you need to for the purpose of the test:
```swift
let store = TestStore(initialState: Feature.State()) {
Feature()
} withDependencies: {
$0.numberFact.fetch = { "\($0) is a good number Brent" }
}
await store.send(.numberFactButtonTapped)
await store.receive(\.numberFactResponse) {
$0.numberFactAlert = "0 is a good number Brent"
}
```
That is the basics of building and testing a feature in the Composable Architecture. There are
_a lot_ more things to be explored, such as <doc:DependencyManagement>, <doc:Performance>,
<doc:SwiftConcurrency> and more about <doc:Testing>. Also, the [Examples][examples] directory has
a bunch of projects to explore to see more advanced usages.
[examples]: https://github.com/pointfreeco/swift-composable-architecture/tree/main/Examples