Integrating Helm with TCA

Hello all,

I'm looking for a correct way of integrating Helm with TCA. I'm the dev of the former and a couple of people asked if this can be done. I've personally never used TCA, still, I'd like to give an idiomatic answer.

Here's what I have so far:

  1. Add present and dismiss actions:
enum AppAction: Equatable {
     //...
     case present(fragment: AppFragment)
     case dismiss(fragment: AppFragment)
}
  1. The state should hold the presented fragments:
struct AppState: Equatable {
     //...
     var presentedFragments: Set<AppFragment>
}
  1. Keep Helm in the environment:
struct AppEnvironment {
     //...
     var helm: Helm = Helm(...)
}
  1. In the reducer, update Helm and the state:
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment in
     switch action {
         case let .present(fragment):
             environment.helm.present(fragment: fragment)
             state.presentedFragments = helm.presentedFragments
             return .none

         case let .dismiss(fragment):
             environment.helm.dismiss(fragment: fragment)
             state.presentedFragments = helm.presentedFragments
             return .none
     }
}
  1. Use TCA in the View as you'd normally do:
.sheet(isPresented: viewStore.binding(
       get: { $0.presentedFragments.contains(.welcomeAlert) },
       send: .dismiss(fragment: .welcomeAlert)))

Now, one thing still bothers me:

Helm has a couple of helper methods that seamlessly integrate with SwiftUI, mainly isPresented(fragment:) for Bool bindings and pickPresented(fragments:) for TabView:

.sheet(isPresented: helm.isPresented(.welcomeAlert)) {}
TabView(selection: helm.pickPresented([.library, .news, .settings])) {}

These are convenient and keep the view in the dark. Is there a way to return these bindings instead of the ones above (using viewStore.binding())? Also, if not, is there a better way to search if a fragment is visible or not without forcing the view to look for it in a set (i.e. expose a direct method to the view). It's a small thing, but trying to keep the view as stupid as possible.

Also, if I'd have to report an error, I suppose storing it in the state would be the way to go?

Finally, is this an idiomatic integration for TCA. Can I use it as an example in my docs, etc?

1 Like

Helm looks interesting but I’m not really seeing a strong use case for integrating it with something like TCA.

The philosophy of TCA is that your app state is the source of truth. It doesn’t make sense to me for your navigation state to be off in some other external thing when the two are really inseparable.

For instance, if you are presenting some modal sheet, the state of this screen will be driven by some kind of optional state within the parent domain. You can’t have one without the other.

Helm looks like a better fit for using with pure SwiftUI.

In terms of idiomatic TCA code, you’re reducer has introduced side effects by calling helm when the reducer is supposed to be side effect free. Calls to helm should be performed in a returned effect.

1 Like

Makes sense. It should be an effect that changes Helm's state and then fires a new action to update the presentedFragments in the app state. Which sort of makes navigation an "external" service.
The navigation is normally handled by toggling flags (i.e. isDashboardVisible) via actions in a TCA app?
Thanks a lot for the reply!