skip to Main Content

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

2

Answers


  1. 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())
    }
    
    
    Login or Signup to reply.
  2. 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.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search