简体   繁体   中英

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'

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)
}

In a lot of cases, the new async model will work out what thread is best suited to execute a task, if your task has access to Guide objects, the compiler/runtime will choose to run the entire task in the main thread, if you have other things you want to run in the main thread, you can use @ MainActor, but the more stuff you force in the main thread, the more work you will put on that thread and the less work will be given to other threads, and so less chance for your work to be spread across multiple cores. If you really push it you can get a task to run in the background thread that access Guide stuff, and you will find execution will jump to the main thread to execute the GUI call. If there are a set of calls you want to run in the main thread with some gui code you code just wrap them in a task, and the the runtime system sort it our for you.

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

Task { @MainActor in 
    print(Thread.current.isMainThread) // "true"
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM