Effect.future closure call is not received / is not processed / is lost

I am trying to fetch notification permissions from the UNUserNotificationCenter and store them in my Store to later use in views.

I've gotten to the point that the getNotificationSettings method is called as a result of an Action and am "returning" the value through a Effect.future closure.
Mapping this to a fetchNotificationPermissionsSettingResult Action and returning the Effect in the reducer.

I am expecting this action to be processed right after the closure call, but nothing happens. The function in map is never called.

I am pulling my hair out trying to figure out why!

Snippets from my code below:

Reducer:

let notificationsPermissionsReducer: Reducer<NotificationPermissionsState, NotificationPermissionsAction, SystemEnvironment<NotificationPermissionsEnvironment>> = Reducer<
NotificationPermissionsState,
NotificationPermissionsAction,
SystemEnvironment<NotificationPermissionsEnvironment>
> { state, action, environment in
  switch action {
  case .fetchNotificationPermissionsSetting:
    return environment.fetchPermissions(environment.notificationCenter())
      .receive(on: environment.mainQueue())
      .catchToEffect(NotificationPermissionsAction.fetchNotificationPermissionsSettingResult)
  case .fetchNotificationPermissionsSettingResult(let result):
    switch result {
    case .success(let isAuthorized):
      state.isAuthorized = isAuthorized
    case .failure(let error):
      break
    }
    return .none
  }
}

Effect:

func fetchPermissions(center: UNUserNotificationCenter) -> Effect<Bool, AppError> {
  .future { closure in
    center.getNotificationSettings { settings in
      closure(.success(settings.authorizationStatus == .authorized))
    }
  }
}

Scene/SceneView sending the initial action:

struct SampleApp: App {
  @Environment(\.scenePhase) private var scenePhase

  var body: some Scene {
    let store = Store(
      initialState: RootState(),
      reducer: rootReducer,
      environment: .live(environment: RootEnvironment()))
    WindowGroup {
      RootView(store: store)
        .onChange(of: scenePhase) { phase in
          if (phase == .active) {
            let viewStore = ViewStore(store)
            viewStore.send(.notificationPermissionsAction(.fetchNotificationPermissionsSetting))
          }
        }
    }
  }
}

This is my first project working with Swift, SwiftUI, Combine and TCA so I have the feeling that I am missing a piece.

Currently you are recreating the store when the body scene is re-computed. This means it gets de-initialized immediately and then kills all inflight effects. You want the store to be long-living, so try hoisting it out of the SampleApp type:

let store = Store(
  initialState: RootState(),
  reducer: rootReducer,
  environment: .live(environment: RootEnvironment())
)

struct SampleApp: App {
  @Environment(\.scenePhase) private var scenePhase

  var body: some Scene {
    ...
  }
}
1 Like

Ah jeez, :man_facepalming:. Of course!

Something with forest and trees...

Thanks a lot!