Importing Data into SwiftData in the Background Using ModelActor and @Query

I have an app with fairly typical requirements - I need to insert some data (in my case from the network but could be anything) and I want to do it in the background to keep the UI responsive.

I'm using SwiftData.

I've created a ModelActor that does the importing and using the debugger I can confirm that the data is indeed being inserted.

On the UI side, I'm using @Query and a SwiftUI List to display the data but what I am seeing is that @Query is not updating as the data is being inserted. I have to quit and re-launch the app in order for the data to appear, almost like the context running the UI isn't communicating with the context in the ModelActor.

I've included a barebones sample project. To reproduce the issue, tap the 'Background Insert' button. You'll see logs that show items being inserted but the UI is not showing any data.

I've tested on the just released iOS 18b3 seed (22A5307f).

The sample project is here:

https://hanchor.s3.amazonaws.com/misc/SwiftDataBackgroundV2.zip

Answered by DTS Engineer in 795327022

I see the issue a bug on SwiftData + SwiftUI side because @Query is supposed to merge the change from the model actor (ModelActor) and trigger a SwiftUI update, and so would suggest that you file a feedback report and post your report ID for folks to track.

For a workaround before the issue is fixed on the framework side, you might consider observing .NSManagedObjectContextDidSave notification and triggering a SwiftUI update from the notification handler. For example:

import Combine

extension NotificationCenter {
    var managedObjectContextDidSavePublisher: Publishers.ReceiveOn<NotificationCenter.Publisher, DispatchQueue> {
        return publisher(for: .NSManagedObjectContextDidSave).receive(on: DispatchQueue.main)
    }
}

struct MySwiftDataView: View {
    @Query private var items: [Item]
    
    // Use the notification time as a state to trigger a SwiftUI update.
    // Use a state more appropriate to your app, if any.
    @State private var contextDidSaveDate = Date()
    
    var body: some View {
        List {
            ForEach(items) { item in
                Text("\(item.timestamp)")
            }
            // Refresh the view by changing its `id`.
            // Use a way more appropriate to your app, if any.
            .id(contextDidSaveDate)
        }
        .onReceive(NotificationCenter.default.managedObjectContextDidSavePublisher{ notification in
            contextDidSaveDate = .now
        }
    }
}

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

Accepted Answer

I see the issue a bug on SwiftData + SwiftUI side because @Query is supposed to merge the change from the model actor (ModelActor) and trigger a SwiftUI update, and so would suggest that you file a feedback report and post your report ID for folks to track.

For a workaround before the issue is fixed on the framework side, you might consider observing .NSManagedObjectContextDidSave notification and triggering a SwiftUI update from the notification handler. For example:

import Combine

extension NotificationCenter {
    var managedObjectContextDidSavePublisher: Publishers.ReceiveOn<NotificationCenter.Publisher, DispatchQueue> {
        return publisher(for: .NSManagedObjectContextDidSave).receive(on: DispatchQueue.main)
    }
}

struct MySwiftDataView: View {
    @Query private var items: [Item]
    
    // Use the notification time as a state to trigger a SwiftUI update.
    // Use a state more appropriate to your app, if any.
    @State private var contextDidSaveDate = Date()
    
    var body: some View {
        List {
            ForEach(items) { item in
                Text("\(item.timestamp)")
            }
            // Refresh the view by changing its `id`.
            // Use a way more appropriate to your app, if any.
            .id(contextDidSaveDate)
        }
        .onReceive(NotificationCenter.default.managedObjectContextDidSavePublisher{ notification in
            contextDidSaveDate = .now
        }
    }
}

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

iOS 18 issue submitted as FB14240514

This workaround worked for me, thanks @DTS Engineer 👍

There's a note on my FB that a fix has been identified for a future release - hopefully!

Glad there is a fix coming - let's hope it makes it in time. This is still broken in iOS 18 / Xcode 16 beta 5.

I'm going to hold off on the "workaround" as it is very heavy handed and inefficient. Any single change to the main context will cause all views driven by Query to redraw, regardless if they need to.

Not sure how this one got past QA...

A couple of weeks ago I also filed FB14504563 ([iOS 18 regression] Saving model context on background thread / ModelActor does not update @Query / SwiftUI views)

This is a pretty clear regression and hopefully fixed before iOS 18 ships

I’m seeing some differences in iOS 18 beta 6 (but still using Xcode 16 beta 5). I won’t call them “improvements” as the feature is still not reliable, but I do see changes propagating to the main context sometimes.

There hasn’t been a single mention of SwiftData in any of the release notes so far: no known issues, nothing fixed. And that’s concerning, as I am seeing plenty of issues in addition to this one.

Yes, it seems something was fixed (using Beta 6 and Xcode 16 b5). In my case I could get rid of the previous workaround with Notification Center and I can see the changes reflecting in the views.

However there are definitely new problems with saving models - in some cases it appears a model's @Relationship is not saved. Will try to recreate this in a test project.

In the meantime, @DTS Engineer - any comments on the new changes?

PS. it is really quite frustrating that there is no mention of SwiftData issues in any of the Release Notes although changes are evidently being made

it seems something was fixed (using Beta 6 and Xcode 16 b5).

Yes, beta 6 is supposed to fix some issues related to SwiftData changes not triggering SwiftUI updates. Glad that folks confirmed the situation was improved.

However there are definitely new problems with saving models - in some cases it appears a model's @Relationship is not saved. Will try to recreate this in a test project.

This may be related to the timing of context saving. You can take a look if the discussion in the following post helps:

For any new problem you see in the beta, I’d suggest that you file a feedback report (http://developer.apple.com/bug-reporting/) and share your report ID here.

Best,
——
Ziqiao Chen
 Worldwide Developer Relations.

I have an app that does an intensive loading of startup information from a network call to SwiftData, and the first time I start the app in iOS 17.5 (the exact same code) it makes the UI slow down and stop responding at times, although as the data is loading it appears. Slow, but it appears. But that same code in iOS 18 beta 5 loads perfectly and quickly (verified) but the UI doesn't update unless I force it. Like everyone notice on that post.

I say this because the behavior in iOS 17.5 was wrong since if it is supposed to be a background thread it should not affect the UI and yet it did. It seemed like in iOS 17 the @MainActor was forced in the context of the ModelActor and even if you did the insertion operations in the background they were actually done in the MainActor and affected the UI as if you did it from a normal view. It would be great if all inserts were done in the background in the ModelActor and that when applying the .save (or maybe a new method called .saveAndNotify) the UI would be notified that it can now update that the data is already there). Like the suggested subscription to the NotificationCenter but automatic.

Thanks a lot in advance.

Thanks for your reply, @DTS Engineer, I'm not sure the issue is related to the timing as in my case the try modelContext.save() is called after each operation. Also the example in the other post seems to be dealing with synchronous operations, rather than async via ModelActor.

Still trying to replicate the exact steps. All I can see that a @Relationship is removed somehow beween two models some time after saving via ModelActor (can see that in CloudKit console), but it works correctly in iOS 17.5

SwiftData still have serious issues, specially when working with CloudKit (iOS 18 Beta). One of the issues I was able to identified is that the views do not update when relationships change. You can see all the values in an object except the values in the relationships (they are nil), but if I force an update on the view, the values show up. I hope they come up with a solution before release, otherwise SwiftData will be completely useless.

I'm experiencing this issue after changing to pass PersistentIdentifiers between Actors instead of the actual model.

I have standings, which are children of an event (one to many), When the children are updated by an @ModelActor, the changes are not reflected in the UI. The parent passed to the view as an @Bindable parameter. The updates to the UI do appear upon relaunching or navigating away and returning. I have a sample project I can share if anyone is interested.

Observing the NotificationCenter, suggested by @DTS Engineer does not resolve this issue. Feedback Submitted: FB15281260

I can still observe the original issue in the current iOS18.0 release version; The workaround also still works but it's been months and more than a few betas before the GM / public release yet this doesn't seem to have been addressed at all?

@DTS Engineer it seems the ModelActor loses it's original idea.. not only me, but a lot of people in my team are having issues with it - specifically modelContainer's modelContext doesn't really save any of changes that have been done. If you pass Model from outside of the ModelActor and use its modelContext now everything works fine, but in that case what is the idea of ModelActor if one need to go with my solution ?

Importing Data into SwiftData in the Background Using ModelActor and @Query
 
 
Q