These are helpful snippets when using TCA.
Binding a TextField
TextField("Search", text: $store.query) // Binds to State.query
enum Action: BindableAction { // Must be a BindableAction
case binding(BindingAction<State>)
}
var body: some ReducerOf<Self> {
BindingReducer() // Add the reducer
Reduce { state, action in
case .binding(\.query):
...
}
}
The use of BindableAction
and BindingReducer
helps in reducing many boilerplate.
Set state in .run
When you run an effect, you often will want to mutate the state. But you can’t do this
return .run { send in
state.query = "sam"
}
Instead, you need to create a new action that mutate that particular state. Tedious that is..
A neater way is to make use of binding to set the keypath:
return .run { send in
await send(.binding(.set(\.query, "wize")))
}
Presentation Sheet
struct State {
@Presents var destination: Destination.State?
}
@Reducer
enum Destination {
case child(ChildFeature)
}
enum Action: BindableAction {
case destination(PresentationAction<Destination.Action>)
}
var body {
Reduce { state, action in { ... }
.ifLet(\.$destination, action: \.destination)
}
When you want to present the sheet, the reducer can set the destination with state.destination = .child(ChildFeature.State())
// Parent view
content
.sheet(
item: $store.scope(
state: \.destination?.child,
action: \.destination.child
),
content: { store in
ChildView(store: store)
}
)
Navigation
struct State {
var path = StackState<Path.State>()
}
@Reducer
enum Path {
case child(ChildFeature)
}
enum Action {
case path(StackActionOf<Path>)
}
var body: some Reducer<State, Action> {
Reduce { state, action in
...
}
.forEach(\.path, action: \.path) // Do not miss out this
}
// In the root view:
var body: some View {
NavigationStack(path: $store.scope(state: \.path, action: \.path)) {
...
} destination: { store in
// For each path type, add the destination view
// This simple example has only child path, that goes to ChildView
switch store.case {
case .child(let store):
ChildView(store: store)
}
}
}
TabView
There is some similarities with Destination and Navigation, but tab needs to keep all the states.
@ObservableState
struct State {
var tab1 = Tab1Feature.State()
var tab2 = Tab2Feature.State()
}
enum Action: BindableAction {
case binding(BindingAction<State>)
case tab1(Tab1Feature.Action)
case tab2(Tab2Feature.Action)
}
In the reducer, provide their scope.
var body: some ReducerOf<Self> {
Reduce { ... }
Scope(state: \.tab1, action: \.tab1) {
Tab1Feature()
}
Scope(state: \.tab2, action: \.tab2) {
Tab2Feature()
}
}
Delegate Pattern
You use the delegate pattern when a child cannot handle certain operation, therefore it delegates to the parent.
Implementation starts from the child:
enum Action {
case delegate(Delegate)
@CasePathable
enum Delegate {
case didFinish(Bool) // eg. true if success
case didSomethingElse
}
}
// Reducer send action as per normal
await send(.delegate(.didFinish(true)))
The above uses a nested action delegate
to group all the delegate actions. This is neat because you know what actions are delegated from child. More importantly, parent should only handle children’s delegate actions (it is technically possible for parent to handle ALL actions, but code wise, if you see parent handling non-delegate, that is bad).
The parent will reduce for:
case .destination(.presented(.child(.delegate(.didFinish(let finish))))):
if finish { ... }
return .none
How parent can send child’s action
Delegate is upward. The reverse is for parent to send action to children.
case .destination(.presented(.child(.delegate(.didFinish(let finish))))):
return .run { send in
await send(.destination(.presented(.child(.someAction))))
}
Picker and selection state
Let’s say you have a segmented control to pick either “Free” or “Paid”. Start with modeling it:
@ObservableState
struct State {
var selection: Selection = .free
enum Selection: LocalizedStringKey, CaseIterable, Hashable {
case free = "Free"
case paid = "Paid"
}
}
You also need to add the view action, which you will use later.
enum Action {
case onTapPicker(State.Selection)
}
// In reducer, simply set it
case .onTapPicker(let selection):
state.selection = selection
return .none
For the Picker
, the selection will bind to the state, at the same time when the selection is changed, it has to send to Action.onTapPicker
(which as above, simply set the state).
Picker("", selection: $store.selection.sending(\.onTapPicker)) {
ForEach(TheFeature.State.Selection.allCases, id: \.self) { selection in
Text(selection.rawValue).tag(selection)
}
}
.pickerStyle(.segmented)
Page View
There is no UIPageController
. But you can use TabView
with the page
style.
Basic SwiftUI App
A scaffolding for a SwiftUI App:
@main
struct TheApp: App {
let store = Store(initialState: HomeFeature.State()) { HomeFeature() }
var body: some Scene {
WindowGroup {
HomeView(store: store)
}
}
}
@Reducer
struct HomeFeature {
@ObservableState
struct State { ... }
enum Action { ... }
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
...
}
}
}
}
struct HomeView: View {
@Bindable var store: StoreOf<HomeFeature>
var body: some View {
...
}
}