Should ModelActor be used to populate a view?

I'm working with SwiftData and SwiftUI and it's not clear to me if it is good practice to have a @ModelActor directly populate a SwiftUI view. For example when having to combine manual lab results and clinial results from HealthKit. The Clinical lab results are an async operation:

@ModelActor
actor LabResultsManager {
    
    func fetchLabResultsWithHealthKit() async throws -> [LabResultDto] {
        let manualEntries = try modelContext.fetch(FetchDescriptor<LabResult>())
        let clinicalLabs = (try? await HealthKitService.getLabResults()) ?? []
        
        return (manualEntries + clinicalLabs).sorted {
            $0.date > $1.date
        }.map {
            return LabResultDto(from: $0)
         }
    }
}

struct ContentView: View {
    @State private var labResults: [LabResultDto] = []
    
    var body: some View {
        List(labResults, id: \.id) { result in
            VStack(alignment: .leading) {
                Text(result.testName)
                Text(result.date, style: .date)
            }
        }
        .task {
            do {
                let labManager = LabResultsManager()
                labResults = try await labManager.fetchLabResultsWithHealthKit()
            } catch {
                // Handle error
            }
        }
    }
}

EDIT: I have a few views that would want to use these labResults so I need an implementation that can be reused. Having to fetch and combine in each view will not be good practice. Can I pass a modelContext to a viewModel?

Answered by DTS Engineer in 854980022

It's unclear what LabResultDto in the code snippet is. If it is a Sendable type that wraps the data in a SwiftData model, that's fine; if it is a SwiftData model type, because a SwiftData model object is not Sendable, you won't want to pass it across actors – Otherwise, Swift 6 compiler will give you an error. For more information about this topic, see the discussion here.

When using SwiftData + SwiftUI, I typically use @Query to create a result set for a view. Under the hood (of @Query), the query controller should be able to detect the changes you made from within a model actor, and trigger a SwiftUI update. Fore more information about observing SwiftData changes, see this WWDC25 video.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

I have a few views that would want to use these labResults so I need an implementation that can be reused. Having to fetch and combine in each view will not be good practice. Can I pass a modelContext to a viewModel?

It's best to use @Query to fetch data when working with UI, like in this case.

ModelActor is is meant to work on background operations.

But SwiftData is also fundamentally broken when merging changes between background and foreground contexts. I would avoid at all costs.

It's unclear what LabResultDto in the code snippet is. If it is a Sendable type that wraps the data in a SwiftData model, that's fine; if it is a SwiftData model type, because a SwiftData model object is not Sendable, you won't want to pass it across actors – Otherwise, Swift 6 compiler will give you an error. For more information about this topic, see the discussion here.

When using SwiftData + SwiftUI, I typically use @Query to create a result set for a view. Under the hood (of @Query), the query controller should be able to detect the changes you made from within a model actor, and trigger a SwiftUI update. Fore more information about observing SwiftData changes, see this WWDC25 video.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

I decided to run this task on app launch to sync the clinical labs into the SwiftData store. This allows me to use Query macro in the views where it's needed and no need to combine lab results. Let me know if this is the right approach:

import SwiftData
import SwiftUI
import OSLog

@ModelActor
actor HealthKitSyncManager {
    private let logger = Logger(subsystem: "com.trtmanager", category: "HealthKitSyncManager")
    
    func sync() async {
        do {
            // Get all Clinical labs
            let hkClinicalRecords = try await TRTHealthKitService.getHKClinicalRecords()
            guard !hkClinicalRecords.isEmpty else { return }

            // Get existing HealthKit IDs in one query
            let descriptor = FetchDescriptor<LabResult>(
//                predicate: #Predicate { $0.source == .clinicalRecord }
            )
            let existing = try modelContext.fetch(descriptor)
                .compactMap { $0.fhirResourceId }
            let existingIDs = Set(existing)
            let missingRecords = hkClinicalRecords.filter { lab in
                guard let fhirResource = lab.fhirResource else { return false }
                return !existingIDs.contains(fhirResource.identifier)
            }
            
            var labResults: [LabResult] = []

            // Insert only new ones
            for record in missingRecords {
                if let fhirResource = record.fhirResource {
                    do {
                        let parsedResults = try TRTHealthKitService.parseFHIRLabResults(fhirResource)
                        labResults.append(contentsOf: parsedResults)
                    } catch {
                        logger.warning("Skipping bad record: \(error.localizedDescription)")
                    }
                }
            }
            
            for labResult in labResults {
                modelContext.insert(labResult)
            }
            
            try modelContext.save()
            logger.info("Successfully synced \(labResults.count) lab results")

        } catch {
            logger.error("Sync failed")
        }
    }
}
Should ModelActor be used to populate a view?
 
 
Q