SwiftData ModelContainer instances don't seem to have a value for setting the Data Protection class.
Is the best way to set that by setting the Data Protection in the app capabilities? Is that the only way?
I have a need for log data that would be "Complete unless open" and user data that would be "Complete", but how do I change one of the containers data protection class?
iCloud & Data
RSS for tagLearn how to integrate your app with iCloud and data frameworks for effective data storage
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
Hello Apple Team,
We’re building a CloudKit-enabled Core Data app and would like clarification on the behavior and performance characteristics of Binary Data attributes with “Allows External Storage” enabled when used with NSPersistentCloudKitContainer.
Initially, we tried storing image files manually on disk and only saving the metadata (file URLs, dimensions, etc.) in Core Data. While this approach reduced the size of the Core Data store, it introduced instability after app updates and broke sync between devices. We would prefer to use the official Apple-recommended method and have Core Data manage image storage and CloudKit syncing natively.
Specifically, we’d appreciate guidance on the following:
When a Binary Data attribute is marked as “Allows External Storage”, large image files are stored as separate files on device rather than inline in the SQLite store.
How effective is this mechanism in keeping the Core Data store size small on device?
Are there any recommended size thresholds or known limits for how many externally stored blobs can safely be managed this way?
How are these externally stored files handled during CloudKit sync?
Does each externally stored Binary Data attribute get mirrored to CloudKit as a CKAsset?
Does external storage reduce the sync payload size or network usage, or is the full binary data still uploaded/downloaded as part of the CKAsset?
Are there any bandwidth implications for users syncing via their private CloudKit database, versus developer costs in the public CloudKit database?
Is there any difference in CloudKit or Core Data behavior when a Binary Data attribute is managed this way versus manually storing image URLs and handling the file separately on disk?
Our goal is to store user-generated images efficiently and safely sync them via CloudKit, without incurring excessive local database bloat or CloudKit network overhead.
Any detailed guidance or internal performance considerations would be greatly appreciated.
Thank you,
Paul Barry
Founder & Lead Developer — Boat Buddy / Vessel Buddy iOS App
Archipelago Environmental Solutions Inc.
I've already submitted this as a bug report to Apple, but I am posting here so others can save themselves some troubleshooting. This is submitted as FB14337982 with an attached complete Xcode project to replicate.
In iOS 17 we use a ModelActor to download data which is saved as an Event, and then save it to SwiftData with a relationship to a Location. In iOS 18 22A5307d we are seeing that this code no longer persists the relationship to the Location, but still saves the Event. If we put a breakpoint in that ModelActor we see that the object graph is correct within the ModelActor stack trace at the time we call modelContext.save(). However, after saving, the relationship is missing from the default.store SQLite file, and of course from the app UI.
Here is a toy example showing how inserting an Employee into a Company using a ModelActor gives unexpected results in iOS 18 22A5307d but works as expected in iOS 17.
It appears that no relationships data survives being saved in a ModelActor.ModelContext.
Also note there seems to be a return of the old bug that saving this data in the ModelActor does not update the @Query in the UI in iOS 18 but does so in iOS 17.
Models
@Model
final class Employee {
var uuid: UUID = UUID()
@Relationship(deleteRule: .nullify) public var company: Company?
/// For a concise display
@Transient var name: String {
self.uuid.uuidString.components(separatedBy: "-").first ?? "NIL"
}
init(company: Company?) {
self.company = company
}
}
@Model
final class Company {
var uuid: UUID = UUID()
@Relationship(deleteRule: .cascade, inverse: \Employee.company)
public var employees: [Employee]? = []
/// For a concise display
@Transient var name: String {
self.uuid.uuidString.components(separatedBy: "-").first ?? "NIL"
}
init() { }
}
ModelActor
import OSLog
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "SimpleModelActor")
@ModelActor
final actor SimpleModelActor {
func addEmployeeTo(CompanyWithID companyID: PersistentIdentifier?) {
guard let companyID,
let company: Company = self[companyID, as: Company.self] else {
logger.error("Could not get a company")
return
}
let newEmployee = Employee(company: company)
modelContext.insert(newEmployee)
logger.notice("Created employee \(newEmployee.name) in Company \(newEmployee.company?.name ?? "NIL")")
try! modelContext.save()
}
}
ContentView
import OSLog
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "View")
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Query private var companies: [Company]
@Query private var employees: [Employee]
@State private var simpleModelActor: SimpleModelActor!
var body: some View {
ScrollView {
LazyVStack {
DisclosureGroup("Instructions") {
Text("""
Instructions:
1. In iOS 17, tap Add in View. Observe that an employee is added with Company matching the shown company name.
2. In iOS 18 beta (22A5307d), tap Add in ModelActor. Note that the View does not update (bug 1). Note in the XCode console that an Employee was created with a relationship to a Company.
3. Open the default.store SQLite file and observe that the created Employee does not have a Company relationship (bug 2). The relationship was not saved.
4. Tap Add in View. The same code is now executed in a Button closure. Note in the XCode console again that an Employee was created with a relationship to a Company. The View now updates showing both the previously created Employee with NIL company, and the View-created employee with the expected company.
""")
.font(.footnote)
}
.padding()
Section("**Companies**") {
ForEach(companies) { company in
Text(company.name)
}
}
.padding(.bottom)
Section("**Employees**") {
ForEach(employees) { employee in
Text("Employee \(employee.name) in company \(employee.company?.name ?? "NIL")")
}
}
Button("Add in View") {
let newEmployee = Employee(company: companies.first)
modelContext.insert(newEmployee)
logger.notice("Created employee \(newEmployee.name) in Company \(newEmployee.company?.name ?? "NIL")")
try! modelContext.save()
}
.buttonStyle(.bordered)
Button("Add in ModelActor") {
Task {
await simpleModelActor.addEmployeeTo(CompanyWithID: companies.first?.persistentModelID)
}
}
.buttonStyle(.bordered)
}
}
.onAppear {
simpleModelActor = SimpleModelActor(modelContainer: modelContext.container)
if companies.isEmpty {
let newCompany = Company()
modelContext.insert(newCompany)
try! modelContext.save()
}
}
}
}
relationshipKeyPathsForPrefetching in SwiftData does not seem to work here when scrolling down the list. Why?
I would like all categories to be fetched while posts are fetched - not while scrolling down the list.
struct ContentView: View {
var body: some View {
QueryList(
fetchDescriptor: withCategoriesFetchDescriptor
)
}
var withCategoriesFetchDescriptor: FetchDescriptor<Post> {
var fetchDescriptor = FetchDescriptor<Post>()
fetchDescriptor.relationshipKeyPathsForPrefetching = [\.category]
return fetchDescriptor
}
}
struct QueryList: View {
@Query
var posts: [Post]
init(fetchDescriptor: FetchDescriptor<Post>) {
_posts = Query(fetchDescriptor)
}
var body: some View {
List(posts) { post in
VStack {
Text(post.title)
Text(post.category?.name ?? "")
.font(.footnote)
}
}
}
}
@Model
final class Post {
var title: String
var category: Category?
init(title: String) {
self.title = title
}
}
@Model final class Category {
var name: String
init(name: String) {
self.name = name
}
}
What is the idiomatic way to use a ModelContext in a document based SwiftData app from a background thread?
The relevant DocumentGroup initializers do not give us direct access to a ModelContainer, only to a ModelContext.
Is it safe to take its modelContext.container and pass it around (for creating a ModelContext on it on a background thread) or to construct a ModelActor with it? Is it safe to e.g. put a ModelActor so created into the environment of the root view of the window and execute various async data operations on it in Tasks throughout the app, as long as these are dispatched from within the window whose root view's ModelContext was used for getting the ModelContainer?
I'm developing a SwiftUI app using SwiftData and encountering a persistent issue:
Error Message:
Thread 1: Fatal error: Duplicate keys of type 'Bland' were found in a Dictionary.
This usually means either that the type violates Hashable's requirements, or that members of such a dictionary were mutated after insertion.
Details:
Occurrence: The error always occurs on the first launch of the app after installation. Specifically, it happens approximately 1 minute after the app starts.
Inconsistent Behavior: Despite no changes to the code or server data, the error occurs inconsistently.
Data Fetching Process:
I fetch data for entities (Bland, CrossZansu, and Trade) from the server using the following process:
Fetch Bland and CrossZansu entities via URLSession.
Insert or update these entities into the SwiftData context.
The fetched data is managed as follows:
func refleshBlandsData() async throws {
if let blandsOnServer = try await DataModel.shared.getBlands() {
await MainActor.run {
blandsOnServer.forEach { blandOnServer in
if let blandOnLocal = blandList.first(where: { $0.code == blandOnServer.code }) {
blandOnLocal.update(serverBland: blandOnServer)
} else {
modelContext.insert(blandOnServer.bland)
}
}
}
}
}
This is a simplified version of my StockListView. The blandList is a @Query property and dynamically retrieves data from SwiftData:
struct StockListView: View {
@Environment(\.modelContext) private var modelContext
@Query(sort: \Bland.sname) var blandList: [Bland]
@Query var users: [User]
@State private var isNotLoaded = true
@State private var isLoading = false
@State private var loadingErrorState = ""
var body: some View {
NavigationStack {
List {
ForEach(blandList, id: \.self) { bland in
NavigationLink(value: bland) {
Text(bland.sname)
}
}
}
.navigationTitle("Stock List")
.onAppear {
doIfFirst()
}
}
}
// This function handles data loading when the app launches for the first time
func doIfFirst() {
if isNotLoaded {
loadDataWithAnimationIfNotLoading()
isNotLoaded = false
}
}
// This function ensures data is loaded with an animation and avoids multiple triggers
func loadDataWithAnimationIfNotLoading() {
if !isLoading {
isLoading = true
Task {
do {
try await loadData()
} catch {
// Capture and store any errors during data loading
loadingErrorState = "Data load failed: \(error.localizedDescription)"
}
isLoading = false
}
}
}
// Fetch data from the server and insert it into the SwiftData model context
func loadData() async throws {
if let blandsOnServer = try await DataModel.shared.getBlands() {
for bland in blandsOnServer {
// Avoid inserting duplicate keys by checking for existing items in blandList
if !blandList.contains(where: { $0.code == bland.code }) {
modelContext.insert(bland.bland)
}
}
}
}
}
Entity Definitions:
Here are the main entities involved:
Bland:
@Model
class Bland: Identifiable {
@Attribute(.unique) var code: String
var sname: String
@Relationship(deleteRule: .cascade, inverse: \CrossZansu.bland)
var zansuList: [CrossZansu]
@Relationship(deleteRule: .cascade, inverse: \Trade.bland)
var trades: [Trade]
}
CrossZansu:
@Model
class CrossZansu: Equatable {
@Attribute(.unique) var id: String
var bland: Bland?
}
Trade:
@Model
class Trade {
@Relationship(deleteRule: .nullify)
var user: User?
var bland: Bland
}
User:
class User {
var id: UUID
@Relationship(deleteRule: .cascade, inverse: \Trade.user)
var trades: [Trade]
}
Observations:
Error Context: The error occurs after the data is fetched and inserted into SwiftData. This suggests an issue with Hashable requirements or duplicate keys being inserted unintentionally.
Concurrency Concerns: The fetch and update operations are performed in asynchronous tasks. Could this cause race conditions?
Questions:
Could this issue be related to how @Relationship and @Attribute(.unique) are managed in SwiftData?
What are potential pitfalls with Equatable implementations (e.g., in CrossZansu) when used in SwiftData entities?
Are there any recommended approaches for debugging "Duplicate keys" errors in SwiftData?
Additional Info:
Error Timing: The error occurs only during the app's first launch and consistently within the first minute.
Apple WTF? What did you do to all my Apps? none of them work in iOS26.1 (all worked in 26.0).
XCode simply says:
CoreData: error: addPersistentStoreWithType:configuration:URL:options:error: returned error NSCocoaErrorDomain (134140) *
SwiftData is supposed to do all these automatically 🤷🏻
I am working on a SwiftUI project using Core Data. I have an entity called AppleUser in my data model, with the following attributes: id (UUID), name (String), email (String), password (String), and createdAt (Date). All attributes are non-optional.
I created the corresponding Core Data class files (AppleUser+CoreDataClass.swift and AppleUser+CoreDataProperties.swift) using Xcode’s automatic generation. I also have a PersistenceController that initializes the NSPersistentContainer with the model name JobLinkModel.
When I try to save a new AppleUser object using:
let user = AppleUser(context: viewContext)
user.id = UUID()
user.name = "User1"
user.email = "..."
user.password = "password1"
user.createdAt = Date()【The email is correctly formatted, but it has been replaced with “…” for privacy reasons】
try? viewContext.save()
I get the following error in the console:Core Data save failed: Foundation._GenericObjCError.nilError, [:]
User snapshot: ["id": ..., "name": "User1", "email": "...", "password": "...", "createdAt": ...]
All fields have valid values, and the Core Data model seems correct. I have also tried:
• Checking that the model name in NSPersistentContainer(name:) matches the .xcdatamodeld file (JobLinkModel)
• Ensuring the AppleUser entity Class, Module, and Codegen are correctly set (Class Definition, Current Product Module)
• Deleting duplicate or old AppleUser class files
• Cleaning Xcode build folder and deleting the app from the simulator
• Using @Environment(.managedObjectContext) for the context
Despite all this, I still get _GenericObjCError.nilError when saving a new AppleUser object.
I want to understand:
1. Why is Core Data failing to save even though all fields are non-nil and correctly assigned?
2. Could this be caused by some residual old class files, or is there something else in the setup that I am missing?
3. What steps should I take to ensure that Core Data properly recognizes the AppleUser entity and allows saving?
Any help or guidance would be greatly appreciated.
I have been using the basic NSPersistentContainer with 100k+ records for a while now with no issues. The database size can fluctuate a bit but on average it takes up about 22mb on device.
When I switch the container to NSPersistentCloudKitContainer, I see a massive increase in size to ~150mb initially. As the sync engine uploads records to iCloud it has ballooned to over 600mb on device. On top of that, the user's iCloud usage in settings reports that it takes up 1.7gb in the cloud.
I understand new tables are added and history tracking is enabled but the size increase seems a bit drastic. I'm not sure how we got from 22mb to 1.7gb with the exact same data.
A few other things that are important to note:
I import all the 100k+ records at once when testing the different containers. At the time of the initial import there is only 1 relation (an import group record) that all the records are attached to.
I save the background context only once after all the records and the import group have been made and added to the context.
After the initial import, some of these records may have a few new relations added to them over time. I suppose this could be causing some of the size increase, but its only about 20,000 records that are updated.
None of the records include files/ large binary data.
Most of the attributes are encrypted.
I'm syncing to the dev iCloud environment.
When I do make a change to a single attribute in a record, CloudKit reports that every attribute has been modified (not sure if this is normal or not )
Also, When syncing to a new device, the sync can take hours - days. I'm guessing it's having to sync both the new records and the changes, but it exponentially gets slower as more records are downloaded. The console will show syncing activity, but new records are being added at a slower rate as more records are added. After about 50k records, it grinds to a halt and while the console still shows sync activity, only about 100 records are added every hour.
All this to say i'm very confused where these issues are coming from. I'm sure its a combination of how i've setup my code and the vast record count, record history, etc.
If anyone has any ideas it would be much appreciated.
I have a Package.swift file that builds and runs from Xcode 15.2 without issue but fails to compile when built from the command line ("swift build"). The swift version is 6.0.3. I'm at wits end trying to diagnose this and would welcome any thoughts.
The error in question is
error: external macro implementation type 'SwiftDataMacros.PersistentModelMacro' could not be found for macro 'Model()'; plugin for module 'SwiftDataMacros' not found
The code associated with the module is very vanilla.
import Foundation
import SwiftData
@Model
public final class MyObject {
@Attribute(.unique) public var id:Int64
public var vertexID:Int64
public var updatedAt:Date
public var codeUSRA:Int32
init(id:Int64, vertexID:Int64, updatedAt:Date, codeUSRA:Int32) {
self.id = id
self.vertexID = vertexID
self.updatedAt = updatedAt
self.codeUSRA = codeUSRA
}
public static func create(id:Int64, vertexID:Int64, updatedAt:Date, codeUSRA:Int32) -> MyObject {
MyObject(id: id, vertexID: vertexID, updatedAt: updatedAt, codeUSRA: codeUSRA)
}
}
Thank you.
I have an app that from day 1 has used Swiftdata and successfully sync'd across devices with Cloudkit. I have added models to the data in the past and deployed the schema and it continued to sync across devices. Sometime I think in June.2025 I added a new model and built out the UI to display and manage it. I pushed a version to Test Flight (twice over a matter of 2 versions and a couple of weeks) and created objects in the new model in Test Flight versions of the app which should push the info to Cloudkit to update the schema.
When I go to deploy the schema though there are no changes. I confirmed in the app that Cloudkit is selected and it's point to the correct container. And when I look in Cloudkit the new model isn't listed as an indes.
I've pushed deploy schema changes anyway (more than once) and now the app isn't sync-ing across devices at all (even the pre-existing models aren't sync-ing across devices).
I even submitted the first updated version to the app store and it was approved and released. I created objects in the new model in production which I know doesn't create the indexes in the development environment. But this new model functions literally everywhere except Cloudkit and I don't know what else to do to trigger an update.
Starting 20th March 2025, I see an increase in bandwidth and latency for one of my CloudKit projects.
I'm using NSPersistentCloudKitContainer to synchronise my data.
I haven't changed any CloudKit scheme during that time but shipped an update. Since then, I reverted some changes from that update, which could have led to changes in the sync behaviour.
Is anyone else seeing any issues?
I would love to file a DTS and use one of my credits for that, but unfortunately, I can't because I cannot reproduce it with a demo project because I cannot travel back in time and check if it also has an increase in metrics during that time.
Maybe an Apple engineer can green-light me filing a DTS request, please.
I have been working on an app for the past few months, and one issue that I have encountered a few times is an error where quick subsequent deletions cause issues with detached tasks that are triggered from some user actions.
Inside a Task.detached, I am building an isolated model context, querying for LineItems, then iterating over those items. The crash happens when accessing a Transaction property through a relationship.
var byTransactionId: [UUID: [LineItem]] {
return Dictionary(grouping: self) { item in
item.transaction?.id ?? UUID()
}
}
In this case, the transaction has been deleted, but the relationship existed when the fetch occurred, so the transaction value is non-nil. The crash occurs when accessing the id. This is the error.
SwiftData/BackingData.swift:1035: Fatal error: This model instance was invalidated because its backing data could no longer be found the store. PersistentIdentifier(id: SwiftData.PersistentIdentifier.ID(backing: SwiftData.PersistentIdentifier.PersistentIdentifierBacking.managedObjectID(0xb43fea2c4bc3b3f5 <x-coredata://A9EFB8E3-CB47-48B2-A7C4-6EEA25D27E2E/Transaction/p1756>)))
I see other posts about this error and am exploring some suggestions, but if anyone has any thoughts, they would be appreciated.
Hello Apple Team,
We are looking at developing an iOS feature on our current development that stores user-generated images as CKAssets in the public CloudKit database, with access control enforced by our app’s own logic (not CloudKit Sharing as that has a limit of 100 shares per device). Each story or post is a public record, and users only see content based on buddy relationships handled within the app.
We’d like to confirm that this pattern is consistent with Apple’s best practices for social features. Specifically:
Is it acceptable to store user-uploaded CKAssets in the public CloudKit database, as long as access visibility is enforced by the app?
Are there any performance or quota limitations (e.g., storage, bandwidth, or user sync limits) that apply to CKAssets in the public database when used at scale?
Would CloudKit Sharing be recommended instead, even if we don’t require user-to-user sharing invitations?
For App Review, is this model (public CKAssets + app-enforced access control) compliant with Apple’s data and security expectations?
Are there any caching or bandwidth optimization guidelines for handling image-heavy public CKAsset data in CloudKit?
Thanks again for your time
Hi,
I've been using Core Data + CloudKit via NSPersistentCloudKitContainer for several years now. Back then I just created my Core Data AND CloudKit fields by hand.
Now the time has come for a little lightweight migration to a new Core Data model, let's say I just needed to add one String attribute.
So I've done the Core Data local migration as usual, then added this to container code:
try? persistentContainer.initializeCloudKitSchema(options: NSPersistentCloudKitContainerSchemaInitializationOptions())
Run. And everything worked great. but…
Now I've noticed that CloudKit created new CKAsset fields for each String attribute that I had in Core Data (about 5 new CKAsset fields). Is this normal!? Why?
! Is it safe to deploy these changes to prod?
ty.
ChatGPT said: "This field is used internally by CloudKit to handle large string values. If the string value is small enough, it is stored in the normal String field, but if it exceeds the size limit (about 1KB), the string is automatically stored as a CKAsset."
I’m building an app that edits files in iCloud and uses an NSFilePresenter to monitor changes.
When a conflict occurs, the system calls presentedItemDidGain(_:).
In that method, I merge the versions by reading the current (canonical) version using NSFileVersion.currentVersionOfItem(at:) and the conflicting ones using NSFileVersion.unresolvedConflictVersionsOfItem(at:).
This generally works, but sometimes, if two devices edit the same file at the same time, each device sees its own local version as the current one. For example:
Device A writes fileVerA (slightly later in real time)
Device B writes fileVerB
On Device A all works fine, currentVersionOfItem returns fileVerA, as expected, and unresolvedConflictVersionsOfItem returns [fileVerB].
But on Device B, currentVersionOfItem returns fileVerB!? And unresolvedConflictVersionsOfItem returns the same, local file [fileVerB], without any hint of the other conflicting version, fileVerA.
Later, the newer version from the Device A arrives on Device B as a normal, non-conflicting update via presentedItemDidChange(_:).
This seems to contradict Apple’s documentation:
“The currentVersionOfItemAtURL: method returns an NSFileVersion object representing what’s referred to as the current file; the current file is chosen by iCloud on some basis as the current “conflict winner” and is the same across all devices.”
Is this expected behavior, or a bug in how iCloud reports file versions?
Hi,
I did cloudkit synchronization using swiftdata.
However, synchronization does not occur automatically, and synchronization occurs intermittently only when the device is closed and opened.
For confirmation, after changing the data in Device 1 (saving), when the data is fetched from Device 2, there is no change.
I've heard that there's still an issue with swiftdata sync and Apple is currently troubleshooting it, is the phenomenon I'm experiencing in the current version normal?
What have people's experience with converting locally stored app data to a more browser based accessible format? Firebase seems expensive, Subabase a bit more challenging, and CloudKit too restrictive.
Exact same app works fine in debug builds, but on release builds I see this stacktrace indicating that assert() was hit.
Incident Identifier: ***
Distributor ID: com.apple.TestFlight
Hardware Model: iPhone14,3
Process: AuditOS [67847]
Path: /private/var/containers/Bundle/Application/***
Identifier: ***
Version: 1.0 (15)
AppStoreTools: 16C5031b
AppVariant: 1:iPhone14,3:18
Beta: YES
Code Type: ARM-64 (Native)
Role: Foreground
Parent Process: launchd [1]
Coalition: ***
Date/Time: 2025-02-11 12:37:54.7801 -0600
Launch Time: 2025-02-11 12:37:33.1737 -0600
OS Version: iPhone OS 18.3 (22D63)
Release Type: User
Baseband Version: 4.20.03
Report Version: 104
Exception Type: EXC_BREAKPOINT (SIGTRAP)
Exception Codes: 0x0000000000000001, 0x000000019d388e2c
Termination Reason: SIGNAL 5 Trace/BPT trap: 5
Terminating Process: exc handler [67847]
Triggered by Thread: 0
Thread 0 Crashed:
0 libswiftCore.dylib 0x000000019d388e2c _assertionFailure(_:_:file:line:flags:) + 264 (AssertCommon.swift:147)
1 SwiftData 0x0000000261842e04 Schema.KeyPathCache.validateAndCache(keypath:on:) + 2628 (Schema.swift:0)
2 SwiftData 0x000000026178cac4 static PersistentModel.keyPathToString(keypath:) + 360 (DataUtilities.swift:36)
3 SwiftData 0x000000026184c9e4 static PersistentModel.fetchDescriptorKeyPathString(for:) + 36 (FetchDescriptor.swift:51)
4 SwiftData 0x00000002617b9770 closure #1 in PredicateExpressions.KeyPath.convert(state:) + 172 (FetchDescriptor.swift:458)
5 SwiftData 0x00000002617b7f48 PredicateExpressions.KeyPath.convert(state:) + 352 (FetchDescriptor.swift:438)
6 SwiftData 0x00000002617bb7ec protocol witness for ConvertibleExpression.convert(state:) in conformance PredicateExpressions.KeyPath<A, B> + 16 (<compiler-generated>:0)
7 SwiftData 0x00000002617baaa0 PredicateExpression.convertToExpressionOrPredicate(state:) + 716 (FetchDescriptor.swift:219)
8 SwiftData 0x00000002617ba6dc PredicateExpression.convertToExpression(state:) + 32 (FetchDescriptor.swift:237)
9 SwiftData 0x00000002617b7cfc PredicateExpressions.Equal.convert(state:) + 328 (:-1)
10 SwiftData 0x00000002617bba08 protocol witness for ConvertibleExpression.convert(state:) in conformance PredicateExpressions.Equal<A, B> + 64 (<compiler-generated>:0)
11 SwiftData 0x00000002617baaa0 PredicateExpression.convertToExpressionOrPredicate(state:) + 716 (FetchDescriptor.swift:219)
12 SwiftData 0x00000002617b7abc PredicateExpression.convertToPredicate(state:) + 28 (FetchDescriptor.swift:244)
13 SwiftData 0x00000002617b7190 nsFetchRequest<A>(for:in:) + 1204 (FetchDescriptor.swift:64)
14 SwiftData 0x0000000261783358 DefaultStore.fetch<A>(_:) + 292 (DefaultStore.swift:496)
15 SwiftData 0x000000026178322c protocol witness for DataStore.fetch<A>(_:) in conformance DefaultStore + 16 (<compiler-generated>:0)
16 SwiftData 0x00000002617847fc asDataStore #1 <A><A1>(_:) in closure #1 in ModelContext.fetch<A>(_:) + 3152 (ModelContext.swift:2590)
17 SwiftData 0x00000002617a74d8 partial apply for closure #1 in ModelContext.fetch<A>(_:) + 100 (<compiler-generated>:0)
18 SwiftData 0x00000002617a7438 closure #1 in ModelContext.enumerateFetchableStores<A>(_:_:) + 208 (ModelContext.swift:2527)
19 SwiftData 0x00000002617a731c specialized ModelContext.enumerateFetchableStores<A>(_:_:) + 200 (ModelContext.swift:2522)
20 SwiftData 0x00000002617a6f08 ModelContext.fetch<A>(_:) + 144 (ModelContext.swift:2534)
21 SwiftData 0x00000002617a6e70 dispatch thunk of ModelContext.fetch<A>(_:) + 56 (:-1)
22 AuditOS 0x00000001041af3f4 0x10419c000 + 78836
23 AuditOS 0x00000001041bebd5 0x10419c000 + 142293
24 AuditOS 0x00000001041bbbf5 0x10419c000 + 130037
25 AuditOS 0x00000001041d8be5 0x10419c000 + 248805
26 AuditOS 0x00000001041bde6d 0x10419c000 + 138861
27 libswift_Concurrency.dylib 0x00000001aa6bfe39 completeTaskWithClosure(swift::AsyncContext*, swift::SwiftError*) + 1 (Task.cpp:497)
The code in question looks like this:
func addRecord<T: MyDtoProtocol>(_ someDTO: T) async throws {
var zone: ZoneModel? = nil
let recordName = someDTO.recordNameType
let fetchDescriptor = FetchDescriptor<T.ModelType> (predicate: #Predicate {$0.recordName == recordName})
> var localEntitites: [T.ModelType] = try modelContext.fetch(fetchDescriptor) <---- I have isolated crash to this line.
Basically for each swiftdata model type I have associatedType for Data Transfer Object type and vice versa.
Hello,
SwiftData is not working correctly with Swift Concurrency. And it’s sad after all this time.
I personally found a regression. The attached code works perfectly fine on iOS 17.5 but doesn’t work correctly on iOS 18 or iOS 18.1.
A model can be updated from the background (Task, Task.detached or ModelActor) and refreshes the UI, but as soon as the same item is updated from the View (fetched via a Query), the next background updates are not reflected anymore in the UI, the UI is not refreshed, the updates are not merged into the main.
How to reproduce:
Launch the app
Tap the plus button in the navigation bar to create a new item
Tap on the “Update from Task”, “Update from Detached Task”, “Update from ModelActor” many times
Notice the time is updated
Tap on the “Update from View” (once or many times)
Notice the time is updated
Tap again on “Update from Task”, “Update from Detached Task”, “Update from ModelActor” many times
Notice that the time is not update anymore
Am I doing something wrong? Or is this a bug in iOS 18/18.1?
Many other posts talk about issues where updates from background thread are not merged into the main thread. I don’t know if they all are related but it would be nice to have
1/ bug fixed, meaning that if I update an item from a background, it’s reflected in the UI, and
2/ proper documentation on how to use SwiftData with Swift Concurrency (ModelActor). I don’t know if what I’m doing in my buttons is correct or not.
Thanks,
Axel
import SwiftData
import SwiftUI
@main
struct FB_SwiftData_BackgroundApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.modelContainer(for: Item.self)
}
}
}
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@State private var simpleModelActor: SimpleModelActor!
@Query private var items: [Item]
var body: some View {
NavigationView {
VStack {
if let firstItem: Item = items.first {
Text(firstItem.timestamp, format: Date.FormatStyle(date: .omitted, time: .standard))
.font(.largeTitle)
.fontWeight(.heavy)
Button("Update from Task") {
let modelContainer: ModelContainer = modelContext.container
let itemID: Item.ID = firstItem.persistentModelID
Task {
let context: ModelContext = ModelContext(modelContainer)
guard let itemInContext: Item = context.model(for: itemID) as? Item else { return }
itemInContext.timestamp = Date.now.addingTimeInterval(.random(in: 0...2000))
try context.save()
}
}
.buttonStyle(.bordered)
Button("Update from Detached Task") {
let container: ModelContainer = modelContext.container
let itemID: Item.ID = firstItem.persistentModelID
Task.detached {
let context: ModelContext = ModelContext(container)
guard let itemInContext: Item = context.model(for: itemID) as? Item else { return }
itemInContext.timestamp = Date.now.addingTimeInterval(.random(in: 0...2000))
try context.save()
}
}
.buttonStyle(.bordered)
Button("Update from ModelActor") {
let container: ModelContainer = modelContext.container
let persistentModelID: Item.ID = firstItem.persistentModelID
Task.detached {
let actor: SimpleModelActor = SimpleModelActor(modelContainer: container)
await actor.updateItem(identifier: persistentModelID)
}
}
.buttonStyle(.bordered)
Button("Update from ModelActor in State") {
let container: ModelContainer = modelContext.container
let persistentModelID: Item.ID = firstItem.persistentModelID
Task.detached {
let actor: SimpleModelActor = SimpleModelActor(modelContainer: container)
await MainActor.run {
simpleModelActor = actor
}
await actor.updateItem(identifier: persistentModelID)
}
}
.buttonStyle(.bordered)
Divider()
.padding(.vertical)
Button("Update from View") {
firstItem.timestamp = Date.now.addingTimeInterval(.random(in: 0...2000))
}
.buttonStyle(.bordered)
} else {
ContentUnavailableView(
"No Data",
systemImage: "slash.circle", //
description: Text("Tap the plus button in the toolbar")
)
}
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button(action: addItem) {
Label("Add Item", systemImage: "plus")
}
}
}
}
}
private func addItem() {
modelContext.insert(Item(timestamp: Date.now))
try? modelContext.save()
}
}
@ModelActor
final actor SimpleModelActor {
var context: String = ""
func updateItem(identifier: Item.ID) {
guard let item = self[identifier, as: Item.self] else {
return
}
item.timestamp = Date.now.addingTimeInterval(.random(in: 0...2000))
try! modelContext.save()
}
}
@Model
final class Item: Identifiable {
var timestamp: Date
init(timestamp: Date) {
self.timestamp = timestamp
}
}