How to send actions from the outside world (e.g. SceneDelegate)?

I create my Store in the scene delegate, and save a reference to it. Even though I have a reference to it, I'm currently stuck on not being able to send actions to that reference.

For example, I would like to send an action when the app enters the background. I thought I would be able to do this via sending it an action, but the send method is not exposed to the outside world.

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  let appStore = Store<AppState, AppAction> = ...

  func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {

    let contentView = AppView(store: appStore)
    // ...
    window.rootViewController = UIHostingController(rootView: contentView)
    // ...
  }

  func sceneDidEnterBackground(_ scene: UIScene) {
    appStore.send(.didEnterBackground) // ❌ Does not compile as `send` is not public
  }
}

Sending actions from the scene delegate is just one example, I could imagine that in the future I have other singletons/classes that would like to emit global events to the store as well.

What's the best way to send these sorts of actions?


One idea is to create a PassthroughSubject environment object which can take in any arbitrary actions. However that has two problems:

  1. This seems like an anti-pattern to expose something that doesn't seem like it wants to be exposed
  2. I don't see a good way to initialize the listener (e.g. typically I initialize the listeners in the view's onAppear method, but if I want the passthrough action to be sceneWillEnterForeground, this is fired before onAppear happens). Here's an example to illustrate the problem
struct AppEnvironment {
  var actionsPassthrough: PassthroughSubject<AppAction, Never> = .init()
}
let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, environment in
  switch action {
  case .onAppear: // called when AppView appears
    if state.isFirstOnAppear {
      state.isFirstOnAppear = false
      return environment.actionsPassthrough.eraseToEffect()
    } else {
      return .none
    }
  case .didEnterForeground: // called on SceneDelegate will enter foreground
    // ...
    return .none
  }
}
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  let appEnvironment = AppEnvironment()
  // ...
  func sceneWillEnterForeground(_ scene: UIScene) {
    // ❌ Does not emit the first time since the .onAppear event hasn't fired yet
    appEnvironment.actionsPassthrough.send(.didEnterForeground)
  }
}

In the above example, ideally the first WillEnterForeground would fire, but it doesn't because the order of operations is:

  1. .didEnterForeground: no-op since onAppear hasn't happened yet.
  2. .onAppear: registers to listen for actionsPassthrough events.

Only subsequent .didEnterForeground will actually fire the event.

1 Like

You're on the right track. It's just that Store doesn't have the send method, you need to go trough a ViewStore for that. You don't need to keep a reference even, I personally just instantiate them on the fly.

ViewStore(store).send(.currentlyReading(.refresh))

I've been using this technique for a while, I'm not sure if there has been any recent change on the framework that changes the way this should be handled. But this works :)

3 Likes