Sending local variable risks causing data races in @MainActor Task

I have written a simple application following MVVM which loads data from API into Listing and displays the Listing on UI.

Here is a View which loads listing data from view model,

struct ListView: View {
    @State var viewModel = ListViewModel()
    var body: some View {
        // Show Data from ViewModel
    }
}

Implementation for ViewModel:

final class ReminderListViewModel {
    var listSource: ListSource() // ListSource is @MainActor annotated
    
    public func fetchList() {
        // API call with callback
        APICall() { result in
            // Process result
            let models = resultingData // resultingData is of Sendable type
            Task { @MainActor [weak self, models] in
                    // Task created to update following code in Main Thread, ultimately this is kind of callback which updates Listing UI with models
                    guard let self = self else { return }
                    let sourceStatus = self.listSource.updateData(models)
                    // Process sourceStatus
                }
        }
    }
}

self.listSource.updateData(models) is where I am getting the below error.

Implementation for ListSource:

@MainActor
final class ListSource {
    var dataArray: [DataModel] = [] // Data Model is a Sendable struct

    func updateData(_ data: [DataModel]) -> Status {
        dataArray = data
        return data.isEmpty ? .empty : .fetched
    }
}

Here, updateData will set list items and update the list on Main Thread as the class is marked with @MainActor.

The issue here is in ViewModel, where Task { @MainActor ... } has reference for models which is causing the warning.

Has anyone any idea on how I can resolve this?

1 Like

If models were truly Sendable, you wouldn't be getting the error. So I'd start by double-checking that.

@KeithBauerANZ I have double checked and confirmed that models are in fact conforming to Sendable. I have attached SS here for the reference.

Still I am getting the above mentioned issue. I also want to highlight that models is a let and I am just passing it for the data update, I am not doing any other processing to it. It's kind of a callback for API.

Something that stands out to me immediately is ReminderListViewModel is participating in concurrency (using Task in that method which captures self), but self is not Sendable. This is a super common source of trouble.

Given that ReminderListViewModel is a UI component, I think you must @MainActor it.

It certainly does seem like this problem, as presented, shouldn't happen with a Sendable-conforming type. Can you double check that the posted code represents the situation you are currently facing, and that warning is still the same? In particular, I'm curious about the "Process result" section that takes in the result and produces a resultingData.

2 Likes

Yes @mattie that @MainActor works but that will bring a lot of work to main thread.

Like, Processing Result is after getting response from API, we need to process whole data array into another array as well as extract different properties from the resulting array to arrange it in different way.

Should not we do all these stuff on background thread? Processing data on main thread might freeze UI a bit.

1 Like

You're right. The problem is you must isolate this type to the MainActor. What I usually do is just carve out a little private function do to the state-independent processing:

private static nonisolated func processData(_ input: SomeSendableInput) async -> SomeSendableResult {
}
1 Like

Thanks @mattie. I will try that out today.

This seems changing implementation in lot of places to successfully migrate to Swift 6.

Thanks for your help.

Since you are already using a lot of concurrency features, I would’ve bridged API call to this world and simplified this a lot. Also, not sure how much processing are there, but I wouldn’t worry about mapping them on main actor at the start — only if you see that this hits UI responsiveness.

@MainActor
final class ReminderListViewModel {
    var listSource: ListSource()
    
    func fetchList() async {
        let responseItems = await asyncAPICall()
        let models = process(responseItems)
        let sourceStatus  = listSource.updateData(models)
        // anything else
    }
}

func asyncAPICall() async -> [ResponseItem] {
    await withCheckedContinuation { c in
        APICall { items in 
            c.resume(returning: items)
        }
    }
}
3 Likes

This still would involve passing models which are not Sendable across isolation boundaries. So in case there is a real need to create them off the main actor and then pass to UI, models should conform to Sendable. If that’s not possible, then either one more mapping to some lightweight structs would be needed, or just perform mapping to models on the main actor (which not necessarily a bad thing).

Thanks @vns. Will check that too.

Thanks @vns and @mattie. Your approach helped me resolving those warnings. Still looking into and will post resolution once ready.

2 Likes