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.
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?.client,
action: \.destination.client
),
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)
}
}
}
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.