How to incorporate BGAppRefreshTask with Composable Architecture?

Hey I want to create a SwiftUI iOS app, using the Composable Architecture. My app is going to function as an accommodation agent, mainly running in the background using BGAppRefreshTask.

Each time the background fetch is running, it's going to fetch listings of accommodations from a website, and then filter through it, and perform a post request (sendMessage) for each accommodation matching the filter. And then save the Id's of those accommodations there were sent a message to.

My question is:

Where do I fit this in, within the Composable Architecture?

A background task is just another input into your application (like the UI, or a timer, or a notification etc.).

You need to be able to do three things:

  • Register the task handler
  • Schedule new tasks
  • Handle running tasks

Task handling should be handled by dispatching and action and letting your reducer take care of it.

You could put the task registration code in the app delegate, or App if you're using the new SwiftUI lifecycle - it's just a small bit of boilerplate that registers a handler for your task identifier - all you want to do in your handler block is send an action to the store. Alternatively, you could dispatch an appLaunched action and move all of your bootstrapping code into a reducer. It's a judgement call.

Scheduling the next task should be handled as an effect. I would create a lightweight abstraction around this by injecting a taskScheduler into your environment so you can test this.

A bit of example code:

enum AppAction {
  ...
  case backgroundTaskRunning(BGAppRefreshTask)
}

/// somewhere in your App or app delegate when the app has launched:
BGTaskScheduler.shared.register(
    forTaskWithIdentifier: "com.example.MyTask",
    using: nil
) { ViewStore(self.store).send(.taskRunning($0)) }

If you have multiple tasks, you'd repeat the above for each task and have a different action for each task instead of a single generic taskRunning action.

Your reducer would do some stuff when the task runs - given the nature of background tasks you're unlikely to be changing any state, so you're probably just firing off a number of effects, including an effect to schedule the next task if necessary. Let's also assume you've encapsulated your entire background refresh logic as an operation that can be wrapped up in a single effect too.

struct AppEnvironment {
    let scheduleTask: (identifier: String, at: Date) -> Effect<Void, Never>
    let performBackgroundFetch: (BGAppRefreshTask) -> Effect<Void, Never>
}

let appReducer = Reducer<....> { state, action, environment in
    switch action {
    case let .backgroundTaskRunning(task):
        return Effect.concatenate(
            environment.performBackgroundFetch(task),
            environment.scheduleTask(identifier: task.identifier, at: someFutureDate)
        )    
    }
}

We pass the task into our background data refresher so it can set up the expiration handler and notify the task when it is completed. On completion of the background refresh the next task will be scheduled.

This is a rather basic approach. A more robust solution might be to just dispatch the background refresh effect here and have it return a new action, e.g. backgroundDataRefreshCompleted(Result) and handle that result elsewhere in your reducer (as the result may effect if and when you schedule the next task).

Does that give you an idea?

1 Like

Sorry I completely forgot to reply. Yes I think I understand. I generally just wanted to make sure it was possible, before I went ahead and decided on this architecture.

Thank you for your reply :)