iosswiftuiconcurrency

SwiftUI Concurrency: Run activity ONLY on background thread


When my content view loads and its a user's first time opening the app, I contact an API.

But, I don't want this to block the main content. The data I receive will never update/affect the UI. So it should fully run in the background.

Right now, it runs like this:

struct ContentView: View {

    @StateObject var settings = Settings()

    var body: some View {

    }
    .task {
            await loadData()
    }

    func loadData() async {
            // Call an api.
            // get some data using URLSession
            settings.data = data
    }
}

I get the following error: [SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates.

And I think I get it: SwiftUI thinks that I want the task to update the UI.

To fix it, I tried:

    .task {
        DispatchQueue.global(qos: .background).async {
            await loadData()
        }
    }

But, I get: Cannot pass function of type '@Sendable () async -> ()' to parameter expecting synchronous function type

How would I use dispatch queue in this case? I'm only targeting iOS 15+.


Solution

  • While using Swift Concurrency system, you can use the Task.detached(...) constructor to spawn an unstructured detached task. This task will run concurrently in the background. You can additionally specify the task priority .background (equivalent to DispatchQueue qos) if high priority of execution is not necessary.

    As the async function you are trying to run updates a property which triggers a view redraw (settings is declared as an ObservedObject and I assume data is a Published property), thus you must set this property from the main actor.

    For this to work, you could do something like this:

    struct ContentView: View {
    
        @StateObject var settings = Settings()
    
        var body: some View {
            // Some view...
        }
        .task {
            await loadData()
        }
    
        func loadData() async {
            await Task.detached(priority: .background) {
                // Call an api.
                // Get some data using URLSession
                await MainActor.run {
                    settings.data = data
                }
            }
        }
    }