Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swift async/await what it the replacement of DispatchQueue.main.async

How I can return to main thread when using async/await concurrency mechanism in new Swift 5.5? Should I just mark function, class with @MainActor. Can I still use DispatchQueue.main.async? Will it be correct? As new mechanism doesn't use GCD and there is no mapping between async tasks and thread like before?

For example I am using SwiftUI List with refreshable

List { }
.refreshable {
    viewModel.fetchData()
}

Is this ok

List { }
.refreshable {
    DispatchQueue.main.async {
      viewModel.fetchData()
    }
}

Or I need to add @MainActor on ViewModel class? I doesn't use async/await in project so using MainActor just for this single refreshable seems redundant, I also doesn't know how adding such attribute influance remaining methods and properties of ViewModel class, they now use Combine.

But on the other hand Xcode displays

runtime: 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.

Moreover after adding @MainActor to ViewModel I am getting multiple warnings like this

Property 'title' isolated to global actor 'MainActor' can not satisfy corresponding requirement from protocol 'OnlineBankingListViewModelProtocol'

like image 779
Michał Ziobro Avatar asked Jan 24 '26 00:01

Michał Ziobro


2 Answers

You asked:

Can I still use DispatchQueue.main.async?

If you are in an async method and want to dispatch something to the main queue, the most literal equivalent would be:

MainActor.run { ... }

But it is more prudent to simply mark the method (or its class) with @MainActor. Not only will this ensure that it runs it on the main thread, but you get compile-time warnings if you attempt to call it from the wrong actor.

So, if your view model is marked with @MainActor, the manual running of the task on the MainActor becomes unnecessary. This is especially true when dealing with published properties of an observed object.

For example, consider:

@MainActor
class ViewModel: ObservableObject {
    @Published var values: [Int] = []

    func fetchData() async {
        let foo = await ...
        values = foo.values
    }
}

And then

struct ContentView: View {
    @ObservedObject var viewModel = ViewModel()

    var body: some View {
        List {
            ...
        }
        .refreshable {
            await viewModel.fetchData()
        }

    }
}

(Note, I made fetchData an async method and await it within refreshable so that the spinner accurately reflects when the async process is running.)

See WWDC 2021 video Swift concurrency: Update a sample app. That is admittedly illustrating the transition of a UIKit app, but includes examples of @MainActor and MainActor.run.


Note, while @MainActor, largely eliminates the need for MainActor.run { … }, there are still some scenarios where you might use this run pattern. Specifically, if you are on some other actor and want to run, for example, three separate @MainActor functions in succession on the main thread, you can wrap the series of them within a single MainActor.run { … } block, thereby running all three with a single dispatch to the main actor, rather than three separate calls.


Above, I focused on the salient portions, but here is my full MCVE:

struct ContentView: View {
    @ObservedObject var viewModel = ViewModel()

    var body: some View {
        List {
            ForEach(viewModel.values, id: \.self) { value in
                Text("\(value)")
            }
        }
        .refreshable {
            await viewModel.fetchData()
        }

    }
}

struct Foo: Decodable{
    let json: [Int]
}

@MainActor
class ViewModel: ObservableObject {
    @Published var values: [Int] = []

    func fetchData() async {
        do {
            let foo = try await object(Foo.self, for: request)
            values = foo.json
        } catch {
            print(error)
        }
    }

    func object<T: Decodable>(_ type: T.Type, for request: URLRequest) async throws -> T {
        let (data, response) = try await URLSession.shared.data(for: request)

        guard let response = response as? HTTPURLResponse else {
            throw URLError(.badServerResponse)
        }

        guard 200 ... 299 ~= response.statusCode else {
            throw ApiError.failure(response.statusCode, data)
        }

        return try JSONDecoder().decode(T.self, from: data)
    }

    var request: URLRequest = {
        let url = URL(string: "https://httpbin.org/anything")!
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.httpBody = "[1,2,3,4,5]".data(using: .utf8)
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        request.addValue("application/json", forHTTPHeaderField: "Accept")

        return request
    }()
}

enum ApiError: Error {
    case failure(Int, Data)
}
like image 136
Rob Avatar answered Jan 25 '26 17:01

Rob


The replacement for DispatchQueue.main.async { foo.bar() } is:

Task { @MainActor in 
    print(Thread.current.isMainThread) // "true"
}
like image 29
scaly Avatar answered Jan 25 '26 17:01

scaly



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!