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 ?

A few observations on the behaviour in iOS 18.0(.0 and .1) as well as the latest 18.1 beta:

  • changes from background contexts seem to sometimes be merged into the main context at irregular intervals
  • disabling autosave on the main context seems to also prevent propagation of any changes from background contexts
  • listening to .NSManagedObjectContextDidSave as suggested by DTS Engineer only helps in the most basic cases

The first two points make it feel like the MainActor context my SwiftUI views use only checks and merges changes whenever the internal autosave is triggered, even if there have been multiple notifications that the underlying NSManagedObjectContext has been saved by other sources.

As an experiment I tried to manually trigger modelContext.save() for the main context whenever I caught a notification for a background save however this doesn't seem to help with merging of background changes (it actually has no effect - the new data is in the store but it never makes it to any @Query update).

With no mentions of SwiftData in any of the Release Notes (18.0.x as well as 18.1 beta) it's anything but easy to keep faith in SwiftData in it's current, very fragile feeling implementation.


One point especially is giving me grey hairs right now - if an @Model contains an array of other @Model's and a property of one of those changes via a background context the changes do not propagate at all even if the workaround by DTS Engineer is applied.

import SwiftData

@Model
final class HistoryRecord {
    // history records are added regularly via a background @ModelActor that also calls .save() on it's modelContext
    var recordedAt: Date
    var logbook: Logbook?
}

@Model
final class Logbook {
    // every time a HistoryRecord is added lastUpdate is also set to the current date
    var lastUpdate: Date
    @Relationship(delete: .cascade)
    var records: [HistoryRecord] = []()
    var shelf: Bookshelf?
}

@Model
final class Bookshelf {
    @Relationship(delete: .cascade)
    var books: [Logbook]
}

struct BookshelfView: View {
    @Environment(\.modelContext) private var modelContext
    @Query shelves: [Bookshelf]
    var body: some View {
        ForEach(shelves) { shelf in
            ForEach(shelf.books) { book in
                // this number doesn't go up at all
                Text("\(book.records.count) records in book")
            }
        }
    }
}

Anyone have any luck on this? I believe I'm seeing the same issue on iOS 18.0 (release) using both Xcode 16.0 and Xcode 16.1beta3.

Issue persists with Xcode 16.1 iOS 18.1 that was released today (October, 28 2024)

@DTS Engineer this issue is still in Xcode 16.2 iOS 18.2 beta 1. I don't understand how this major bug is still not fixed by now... Can someone at Apple seriously look into this bug?

Also, instead of observing the NSManagedObjectContextDidSave notification (which is part of Core Data + NSManagedObjectContext), we can observe the ModelContext didSave notification. The userInfo dictionary contains the PersistentIdentifiers of inserted/updated/deleted models, so we can only update views using changed models.

extension NotificationCenter {
    var modelContextDidSavePublisher: Publishers.ReceiveOn<NotificationCenter.Publisher, DispatchQueue> {
        return publisher(for: ModelContext.didSave, object: nil).receive(on: DispatchQueue.main)
    }
}

Documentation: https://developer.apple.com/documentation/swiftdata/modelcontext/didsave

As of Xcode 16.3 beta 2, this issue is still not resolved.

Additionally, updating the id (however that is achieved), as per the workaround in this thread, no longer triggers the Query to refresh;-

I just added a button to the form that updates the id, and, after performing updates via a background modelActor, pushing the button did not trigger a refresh.

16.4.0-Beta - not fixed and ModelActor not usable due missing Query refresh.

IS this fixed? I'm still experiencing this in 18.4 GM version of the iOS OS

One point especially is giving me grey hairs right now - if an @Model contains an array of other @Model's and a property of one of those changes via a background context the changes do not propagate at all even if the workaround by DTS Engineer is applied.

Sadly, this still seems to be broken even in iOS 26 beta.

The one workaround that seems to work is to add the related model to the FetchDescriptor.relationshipKeyPathsForPrefetching pased to a Query (for directly related models) or add a @Query specifically for the related models.

For the latter, one need not use the actual result of the @Query itself (though the results must be faulted, by for instance, calling items.isEmpty). Rather simply fetching them through a @Query appears to allow update notifications to work even if a view uses a model accessed via a relationship.

For instance. Suppose one has two models, Shelf and Item. Shelf.items is [Item] and Item.shelf is Shelf?.

struct ListShelvesView: View {
    @Query private var shelves: [Shelf]

    var body: some View {
        List {
            ForEach(shelves) { shelf in
                NavigationLink {
                    ShelfView(shelf: shelf)
                } label: {
                    Text("Shelf \(shelf.name)")
                }
            }
        }
    }
}

struct ShelfView: View {
    var shelf: Shelf

    // not used directly
    @Query var dummy: [Item]

    init(shelf: Shelf) {
        self.shelf = shelf
        let shelfIdentifier = shelf.persistentModelID

        // Setup query to grab related models
        var fd = FetchDescriptor<Item>()
        fd.predicate = #Predicate {
            if let shelf = $0.shelf {
                shelf.persistentModelID == shelfIdentifier
            } else {
                false
            }
        }
        self._dummy = Query(fd)
    }

    var body: some View {
        // fault dummy query
        let _ = dummy.isEmpty

        VStack {
            Text("Shelf \(shelf.name)")
            List {
                ForEach(shelf.items) { item in
                    ListItemView(item: item)
                }
            }
        }
    }
}

struct ListItemView: View {
    @Environment(\.dataActor) private var dataActor
    var item: Item
    
    var body: some View {
        HStack {
            Text("Item \(item.timestamp)")
            Button("Modify") { changeItem() }
        }
    }
    
    private func changeItem() {
        // Update item timestamp on a ModelActor
        Task {
            try await dataActor.updateItem(identifier: item.persistentModelID, timestamp: Date())
        }
    }
}

The downside to this is that it will end up running queries twice, once when accessing shelf.items and once when faulting the dummy query.

One could also just use the results of the dummy query instead, but in cases where you might be accessing multiple related models multiple degrees away (related models of related models), it can be cumbersome to pass those around to views.

I'm writing to express my frustration and disbelief that this critical bug is still present in production as of 2026.

We just completed a major refactoring effort, migrating our application from iCloud-based static file storage to SwiftData with CloudKit sync. During this migration, we encountered this exact issue and was stunned to discover it was already documented in 2024.

Here one of several specific scenarios:

  1. Logbook view structure: @​Bindable var logbook: ​Logbook (where Logbook is a @​Model class)
  2. The logbook​.flights relationship array is not observed - changes to the array don't trigger view updates
  3. Attempted workaround using @​Query var flights: [​Flight] - still not observed
  4. This affects one of our primary user-facing views (flight logbook table)

Why the suggested workaround doesn't work:

The proposed solution of triggering refreshes on model​Context​.did​Save notifications is impractical for real-world applications:

• Context saves can occur multiple times throughout the app's lifecycle • Frequent UI refreshes cause jarring user experience • Table selections are lost on every refresh • State management becomes a nightmare when you can't trust the observation system

This is unacceptable:

• It's been two years since this was first reported • SwiftData was marketed as a production-ready replacement for Core Data • Basic observation of to-many relationships should be fundamental functionality • The workarounds compromise UX and require architectural gymnastics

We need either:

  1. A proper fix for relationship observation
  2. Official guidance on the correct architectural pattern
  3. Acknowledgment that either SwiftData or @Query aren't ready for production use with CloudKit sync

This bug is blocking legitimate use cases and forcing developers to choose between broken observation or constant, disruptive UI refreshes.

When can we expect a real solution?

Importing Data into SwiftData in the Background Using ModelActor and &#64;Query
 
 
Q