Post

Replies

Boosts

Views

Activity

Reply to How to get PersistentIdentifier from a model created in a transaction?
[quote='854498022, DTS Engineer, /thread/797050?answerId=854498022#854498022'] item.persistentModelID should be valid, even before item is persisted to the store. [/quote] I must be missing something. It appears to return a temporary Identifier that can't be fetched. @ModelActor actor DataActor { func createItem() throws { var newIdentifier: PersistentIdentifier? = nil try modelContext.transaction { let item = Item(timestamp: Date()) modelContext.insert(item) newIdentifier = item.persistentModelID print("Created Item: \(item.timestamp)") } print("New identifier: \(String(describing: newIdentifier!))") if let model: Item = try existingModel(for: newIdentifier!) { print("Found model with \(model.timestamp)") } else { print("Failed to load model") } } func existingModel<T: PersistentModel>(for persistentIdentifier: PersistentIdentifier) throws -> T? { if let model: T = modelContext.registeredModel(for: persistentIdentifier) { return model } var fetchDescriptor = FetchDescriptor<T>(predicate: #Predicate { $0.persistentModelID == persistentIdentifier }) fetchDescriptor.fetchLimit = 1 return try modelContext.fetch(fetchDescriptor).first } } Output: Created Item: 2025-08-20 18:33:55 +0000 New identifier: PersistentIdentifier(id: SwiftData.PersistentIdentifier.ID(backing: SwiftData.PersistentIdentifier.PersistentIdentifierBacking.temporaryIdentifier(SwiftData.TemporaryPersistentIdentifierImplementation))) Failed to load model
2w
Reply to Importing Data into SwiftData in the Background Using ModelActor and @Query
[quote='808703022, Shirk, /thread/759364?answerId=808703022#808703022, /profile/Shirk'] 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. [/quote] 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.
2w
Reply to SwiftData relationshipKeyPathsForPrefetching not working
It looks like I can work around part of the problem by executing the fetch for the related models directly. SwiftData will then properly use them as a cache. // Fetch accounts var fd = FetchDescriptor<Account>() let accounts = modelContext.fetch(fd) // CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZACCOUNTID FROM ZACCOUNT t0 // CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZORDERID, t0.ZTIMESTAMP, t0.ZACCOUNT, t0.ZORDERITEMS FROM ZORDER t0 WHERE t0.ZACCOUNT IN (SELECT * FROM _Z_intarray0) ORDER BY t0.ZACCOUNT // Fetch related orders for accounts let accArray = accounts.map { $0.persistentModelID } var fd2 = FetchDescriptor<Order>() fd2.predicate = #Predicate { if let account = $0.account { accArray.contains(account.persistentModelID) } else { false } } let orders = try? modelContext.fetch(fd2) // CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZORDERID, t0.ZTIMESTAMP, t0.ZACCOUNT, t0.ZORDERITEMS FROM ZORDER t0 WHERE (CASE ((CASE ( t0.ZACCOUNT IS NOT NULL) when 1 then ((CASE ( t0.ZACCOUNT IN (SELECT * FROM _Z_intarray0) ) when 1 then (?) else (?) end)) else (?) end) IS NOT NULL) when 1 then ((CASE ( t0.ZACCOUNT IS NOT NULL) when 1 then ((CASE ( t0.ZACCOUNT IN (SELECT * FROM _Z_intarray1) ) when 1 then (?) else (?) end)) else (?) end)) else (?) end) = ? // + 1 query to OrderItem per result if not using an intermediary model for account in accounts { if let orders = account.orders { for order in orders { // this will use the cached order fetched above so no queries will execute let orderID = order.orderID } } } To keep SwiftData from executing a query for each Order to get the PKs for each OrderItem though seems to require replacing the 1-to-many relationship with a 1-to-1 relationship with an intermediary model since SwiftData only pre-fetches on to-many relationships. This isn't particularly ergonomic, but it seems to work.
Mar ’25
Reply to SwiftData relationshipKeyPathsForPrefetching not working
I also filed one for the same issue: FB16858906 In addition, prefetching related models will also prefetch pks for any one-to-many models in the related model which is unexpected and not desired. Given: @Model final class OrderItem { var quantity: Int var sku: Int var order: Order? = nil init(quantity: Int, sku: Int) { self.quantity = quantity self.sku = sku } } @Model final class Order { var orderID: Int var timestamp: Date = Date() var account: Account? @Relationship(deleteRule: .cascade, inverse: \OrderItem.order) var orderItems: [OrderItem]? = [] init(orderID: Int) { self.orderID = orderID } } @Model final class Account { var accountID: Int @Relationship(deleteRule: .cascade, inverse: \Order.account) var orders: [Order]? = [] init(accountID: Int) { self.accountID = accountID } } With some sample data: let account = Account(accountID: 1) modelContext.insert(account) let order = Order(orderID: 100) modelContext.insert(order) order.account = account let orderItem = OrderItem(quantity: 1, sku: 999) modelContext.insert(orderItem) orderItem.order = order Trying to fetch Accounts and pre-fetch related Orders results in 5 queries rather than 2: var fd = FetchDescriptor<Account>() fd.relationshipKeyPathsForPrefetching = [\.orders] let accounts = modelContext.fetch(fd) // CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZACCOUNTID FROM ZACCOUNT t0 // CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZORDERID, t0.ZTIMESTAMP, t0.ZACCOUNT FROM ZORDER t0 WHERE t0.ZACCOUNT IN (SELECT * FROM _Z_intarray0) ORDER BY t0.ZACCOUNT // CoreData: sql: SELECT 0, t0.Z_PK FROM ZORDERITEM t0 WHERE t0.ZORDER = ? for account in accounts { if let orders = account.orders { for order in orders { let orderID = order.orderID // CoreData: sql: SELECT 0, t0.Z_PK, t0.Z_OPT, t0.ZORDERID, t0.ZTIMESTAMP, t0.ZACCOUNT FROM ZORDER t0 WHERE t0.Z_PK IN (?) // CoreData: sql: SELECT 0, t0.Z_PK FROM ZORDERITEM t0 WHERE t0.ZORDER = ? } } }
Mar ’25
Reply to Can't batch delete with one-to-many to self relationship
Since order of database records appears to be an issue, sticking a context.save() after inserting the root node fixes the problem. let root = TreeNode() context.insert(root) try context.save() for _ in 0..<100 { ... However, this workaround isn't usable if there are mutations. For instance, creating a new branch node and moving all the root.children to it would lead to the same problem. So would creating a new parent node of root later. Internally this appears to be caused by this SQLite trigger created by Core Data: CREATE TEMPORARY TRIGGER IF NOT EXISTS ZQ_ZTREENODE_TRIGGER AFTER DELETE ON ZTREENODE FOR EACH ROW BEGIN DELETE FROM ZTREENODE WHERE ZPARENT = OLD.Z_PK; SELECT RAISE(FAIL, 'Batch delete failed due to mandatory OTO nullify inverse on TreeNode/parent') FROM ZTREENODE WHERE Z_PK = OLD.ZPARENT; END If the deletes happened such that parents were always deleted before children, this would work. Unfortunately that's not the case.
Jan ’25
Reply to NavigationLink on item from Query results in infinite loop
I ran into a similar problem. It seems if a NavigationStack is placed in the parent view of a child view with a @Query with predicate, the child view will get invalidated when navigating to or back from the destination view. It seems if the destination View also references the model, that too will get invalidated leading to it navigating back before trying to navigate to the new item leading to a loop. I managed to work around it a couple ways, but the simplest is adding a dummy intermediate View between the View with the NavigationStack and the one with the @Query with predicate like this: struct ContentView: View { var body: some View { NavigationStack { DummyView() } } } struct DummyView: View { var body: some View { ListsView() } } struct ListsView: View { @Environment(\.modelContext) private var modelContext @Query(filter: #Predicate<Item> { _ in true }) // ... }
Topic: UI Frameworks SubTopic: SwiftUI Tags:
May ’24
Reply to Spurious View invalidation with NavigationStack and @Query with a predicate
I seem to have come up with a workaround. Placing a View between the View containing the NavigationStack and the one containing the @Query with predicate filter appears to solve the problem. The view graph no longer gets invalidated when clicking to navigate away or back. The resulting code looks like this: struct ContentView: View { var body: some View { NavigationStack { let _ = Self._printChanges() MiddleView() .navigationDestination(for: Item.self) { item in Text("Item at \(item.num)") } } } } struct MiddleView: View { var body: some View { let _ = Self._printChanges() SubView() } } struct SubView: View { @Environment(\.modelContext) private var modelContext @Query(filter: #Predicate<Item> { item in item.num < 20 }, sort: \.num) private var items: [Item] var body: some View { let _ = Self._printChanges() List { ForEach(items) { item in NavigationLink(value: item) { Text("Item \(item.num)") }.background(Color.random()) } } } } Not only that, but it appears that using NavigationLink(destination:, label: ) in the SubView seems to work now as well whereas before it would sometimes cause an infinite loop when navigating from a view with a Query predicate to another view with one.
Topic: UI Frameworks SubTopic: SwiftUI Tags:
May ’24