Godlike
Godlike

Reputation: 1948

Swift 6 - Switch from @MainActor func to async function

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

Answers (2)

malhal
malhal

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

lorem ipsum
lorem ipsum

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

  • Value types
  • Reference types with no mutable storage
  • Reference types that internally manage access to their state
  • Functions and closures (by marking them with @Sendable)

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

Related Questions