Grouping TCA Presented Stores into Single

Lee young-jun
3 min readMay 24, 2024

--

Today I followed the tutorial and found new one, so I improved my Store with the introduced way.

Search Store had used Add Store directly.

@Reducer
public struct SearchCharacterStore {
@ObservableState
public struct State: Equatable {
@Presents
public var addDateState: AddDateStore.State?

public enum Action {
case addDateAction(PresentationAction<AddDateStore.Action>)

We need to write bolierplate codes to add another nested stores and we can’t ensure only one child store is created now. But TCA provides us a convenient way, it is @Reducer

First define nested Destination enum with the macro, don’t forget .equatable

public extension SearchCharacterStore {
@Reducer(state: .equatable)
public enum Destination {
case addCharacter(AddDateStore)
}
}

And add destination definitions into the Store

@ObservableState
public struct State: Equatable {
...
@Presents
var destination: Destination.State?
...

public enum Action {
case destination(PresentationAction<Destination.Action>)

Compiler will emit error if we don’t give .equatable. Reducer macro will create State and Action automatically for us 😀. So now we can replace PresentationAction Definitions.

// @Presents public var addDateState: AddDateStore.State?
...
// case addDateAction(PresentationAction<AddDateStore.Action>)

This task will raise compile errors.

Therefore we have to modify Reduce also. Replace lines of the code modifying state of specific store to the code using destination state.

state.destination = .addCharacter(.init(selectedDate: now))

The destination will provide autocompletion!

It generates also presented actions.

case let .destination(.presented(.addCharacter(.delegate(.saveDate(savedDate: selectedDate))))):
// case .addDateAction(.presented(.delegate(.saveDate(let selectedDate)))):
return .run { @MainActor send in
database.insert(Item(timestamp: selectedDate))
}
case .destination:
// case .addDateAction:
return .none

Final task is to replace IfLet . I created a new Store in the closure like this.

.ifLet(\.$addDateState, action: \.addDateAction) {
AddDateStore()
}

It is necessary no more. Just fill with only destination.

.ifLet(\.$destination, action: \.destination)

Testing

I wrote tests in previous article, so I modified also it.

I can modify this test code

await store.send(.addButtonTapped) { [now = now] in
$0.destination = .init(selectedDate: now)
}

like this

await store.send(.addButtonTapped) { [now = now] in
$0.destination = .addCharacter(.init(selectedDate: now))
}

And this

await store.send(\.addDateAction.saveButtonTapped) {
$0.addDateState?.isSaving = true
}

can be replaced with

await store.send(\.destination.addCharacter.saveButtonTapped) {
$0.destination?.addCharacter?.isSaving = true
}

There is no special thing, just replace all accessor to the state or the action with destination and press .

That’s all!

Ah… I found one special case. I handled dismiss dependency like following

await store.receive(\.destination.presented.addCharacter.dismiss)

However it is not working.

Because I don’t have to know from where dismiss came? Also the state checking should use destination, not destination?.addCharacter.

await store.receive(\.destination.dismiss) {
$0.destination = nil
}

If you found any helpful from this post, please give it a round of applause 👏. Explore more iOS-related content in my other posts.

You can give me claps up to 50 if you keep pressing the button :)

For additional insights and updates, check out my LinkedIn profile. Thank you for your support!

Troubleshootings

does not conform to protocol ‘Equatable’

@Reducer(state: .equatable)

state autocompletion is not working

Remove or comment out presented cases

Enum case in a public enum uses an internal type

Make Destination public

public enum Destination

References

--

--

No responses yet