Running TCA headless on a background thread

I understand it makes sense to run TCA on the main thread for SwiftUI and UIKit but it is not always desired. For example in one of my apps, we use Texture/AsyncDisplayKit; the configuration and layout of cell nodes is done entirely in the background. We also have a synchronisation task that processes a lot of data from remote. I'd love to refactor the synchronisation code to use TCA, however our user experience will definitely suffer dramatically if that processing would happen on the main thread.

@stephencelis @mbrandonw Is there a way run TCA on a thread other than the main thread?

We try to note this in the README's FAQ, but TCA does not have an opinion on threading, and does no work to run on the main thread. The fact that it typically runs on the main thread in SwiftUI applications is due to the fact that most SwiftUI actions are sent on the main thread. If you want to run TCA on a background queue, you need to send actions to the store on that queue, and your reducer must receive effects on that queue.

Generally, reducers should be lightweight enough to run on the main queue. If you have a task that processes a lot of data that might block your reducer, you can move that work into an Effect and feed the result back through via an action.

1 Like

Thanks! That's great!

@stephencelis could you share some example how Effect can be run on background thread?

In my case I have some quite large collections which I have to iterate. So would be cool to move this opertaions to backround thread.

Thank you in advance for any suggestion!

This would be a great case-study/question on its own, however I imagine this work being a part of an environment:

struct FooEnvironment {
    var scheduler: AnyScheduler<DispatchQueue>
    var doJob: (String, AnyScheduler<DispatchQueue>) -> Effect<Result,Error>
}

When creating this environment you could pass any scheduler you want (you could use main, test or custom dispatch queue here) then in reducer you could do something like this:

case .doHeavyLifting(let param):
    return fooEnv.doJob(param, fooEnv.scheduler)
        .map(.didAGreatJob) // FooAction.didAGreatJob(Result)
        .catch { Just(.failedToDoJob($0.localizedDescription) }  // in case of error
        .eraseToEffect()
1 Like

@stephencelis If I use a recursive reducer and navigate a few level down. Processing actions will slows down my scrollview drastically when an action is processed. Disabling .debug logging helps a little but not much.

I extended view store send to use a background thread and the user interaction lag disappeared completely.

objectWillChange.send() is now called from a background thread and that is not allowed. I think TCA should be modified so that objectWillChange.send() is always called on the main thread.

https://github.com/pointfreeco/swift-composable-architecture/pull/327

Thank you @eimantas for your reply!!!!