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.

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 :)

Terms of Service

Privacy Policy

Cookie Policy