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?