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.

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

Image

@samwize

¯\_(ツ)_/¯

Back to Home