Reputation: 1948
How can I call in Swift 6 a normal async function from a MainActor function? I do not want to execute URLRequests
on the MainThread
I want to switch from the Main Thread to an async Thread but I have no idea how to do it...
This code doesn't work and throws the following error: Sending 'self.service' risks causing data races
import SwiftUI
struct CustomView: View {
private let viewModel: CustomViewModel = CustomViewModel()
var body: some View {
Text("HELLO")
.task {
Task {
await viewModel.getDataFromBackend()
}
}
}
}
@Observable
final class CustomViewModel {
private let service: CustomService = DefaultCustomService()
@MainActor func getDataFromBackend() async {
// Updating UI on Main-Thread
// This shouldn't run on the Main Thread!
await service.provideBackendData()
}
}
protocol CustomService {
func provideBackendData() async
}
struct DefaultCustomService: CustomService {
func provideBackendData() async {
// Async Stuff which shouldn't run on Main-Thread
}
}
Upvotes: 3
Views: 657
Reputation: 30736
Currently your async func is an @Observable
class which is for model data not for services, i.e. its designed to hold shared mutable state which is not usually something a service does. To fix your data races warning your async func can either be static (i.e. no risk of shared mutatble state) or in a sendable struct. To use async/await in SwiftUI with Swift 6 with a mockable service try something like this:
import SwiftUI
struct Result: Identifiable {
let id = UUID()
let title: String
}
extension EnvironmentValues {
@Entry var controller: IController = Controller()
}
protocol IController: Sendable {
func fetchResults() async throws -> [Result]
}
struct Controller: IController {
func fetchResults() async throws -> [Result] {
[.init(title: "Result 1"), .init(title: "Result 2")]
}
}
struct PreviewController: IController {
func fetchResults() async throws -> [Result] {
[.init(title: "Preview Result 1"), .init(title: "Preview Result 2")]
}
}
struct ContentView: View {
@Environment(\.controller) var controller
@State var results: [Result] = []
var body: some View {
NavigationStack {
Form {
ForEach(results) { result in
Text(result.title)
}
}
}
.task {
do {
results = try await controller.fetchResults()
}
catch {}
}
}
}
#Preview {
ContentView()
.environment(\.controller, PreviewController())
}
Upvotes: -2
Reputation: 29614
You can just add
protocol CustomService: Sendable {
To remove the error.
In order to safely make something Sendable
you should meet all of these official requirements
https://developer.apple.com/documentation/swift/sendable
Unofficially any reference types should also be marked final
so the user cant create unsafe mutating children.
You can also use actor
and globalActor
to make something Sendable
.
Upvotes: 2