read

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)
        }
    )
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.


Image

@samwize

¯\_(ツ)_/¯

Back to Home