Home > Back-end >  Swift async/await what it the replacement of DispatchQueue.main.async
Swift async/await what it the replacement of DispatchQueue.main.async

Time:02-13

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'

CodePudding user response:

You asked:

Can I still use DispatchQueue.main.async?

In general, if you are in an async method and want to dispatch something to the main queue, you would use:

MainActor.run { ... }

But, 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.


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

CodePudding user response:

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.

  • Related