What's the correct way to delete a SwiftData model that is in a many to many relationship?

The deletion is working, but it does not refresh the view. This is similar to a question I asked previously but I started a new test project to try and work this out.

@Model
class Transaction {
    var timestamp: Date
    var note: String
    
    @Relationship(deleteRule: .cascade) var items: [Item]?
    
    init(timestamp: Date, note: String, items: [Item]? = nil) {
        self.timestamp = timestamp
        self.note = note
        self.items = items
    }
    
    func getModifierCount() -> Int {
        guard let items = items  else { return 0 }
        
        return items.reduce(0, {result, item in
            result + (item.modifiers?.count ?? 0)
        })
    }
}
@Model
class Item {
    var timestamp: Date
    var note: String
    
    @Relationship(deleteRule: .nullify) var transaction: Transaction?
    @Relationship(deleteRule: .noAction) var modifiers: [Modifier]?
    
    init(timestamp: Date, note: String, transaction: Transaction? = nil, modifiers: [Modifier]? = nil) {
        self.timestamp = timestamp
        self.note = note
        self.transaction = transaction
        self.modifiers = modifiers
    }
}
@Model
class Modifier {
    var timestamp: Date
    var value: Double
    
    @Relationship(deleteRule: .nullify) var items: [Item]?
    
    init(timestamp: Date, value: Double, items: [Item]? = nil) {
        self.timestamp = timestamp
        self.value = value
        self.items = items
    }
}
struct ContentView: View {
    
    @Environment(\.modelContext) private var context
    @Query private var items: [Item]
    @Query private var transactions: [Transaction]
    @Query private var modifiers: [Modifier]
    
    @State private var addItem = false
    @State private var addTransaction = false

    var body: some View {
        NavigationStack {
            List {
                Section(content: {
                    ForEach(items) { item in
                        LabeledText(label: item.timestamp.formatAsString(), value: .int(item.modifiers?.count ?? -1))
                    }
                    .onDelete(perform: { indexSet in
                        withAnimation {
                            for index in indexSet {
                                context.delete(items[index])
                            }
                        }
                    })
                }, header: {
                    LabeledView(label: "Items", view: {
                        Button("", systemImage: "plus", action: {})
                    })
                })
                
                Section(content: {
                    ForEach(modifiers) { modifier in
                        LabeledText(label: modifier.timestamp.formatAsString(), value: .currency(modifier.value))
                    }
                    .onDelete(perform: { indexSet in
                        indexSet.forEach { index in 
                            context.delete(modifiers[index])
                        }
                    })
                }, header: {
                    LabeledView(label: "Modifiers", view: {
                        Button("", systemImage: "plus", action: {})
                    })
                })
                
                Section(content: {
                    ForEach(transactions) { transaction in
                        LabeledText(label: transaction.note, value: .int(transaction.getModifierCount()))
                    }
                    .onDelete(perform: { indexSet in
                        withAnimation {
                            for index in indexSet {
                                context.delete(transactions[index])
                            }
                        }
                    })
                }, header: {
                    LabeledView(label: "Transactions", view: {
                        Button("", systemImage: "plus", action: {addTransaction.toggle()})
                    })
                })
            }
            .navigationTitle("Testing")
            .sheet(isPresented: $addTransaction, content: {
                TransactionEditor()
            })
        }
    }
  
    }
}


Here's the scenario. Create a transaction with 1 item. That item will contain 1 modifier. ContentView will display Items, Modifiers, and Transactions. For Item, it will display the date and how many modifiers it has. Modifier will display the date and its value. Transactions will display a date and how many modifiers are contained inside of its items.

When I delete a modifier, in this case the only one that exist, I should see the count update to 0 for both the Item and the Transaction. This is not happening unless I close the application and reopen it. If I do that, it's updated to 0. I tried to add an ID variable to the view and change it to force a refresh, but it's not updating.

This issue also seems to be only with this many to many relationship. Previously, I only had the Transaction and Item models. Deleting an Item would correctly update Transaction, but that was a one to many relationship.

I would like for Modifier to have a many to many relationship with Items, so they can be reused.

Why is deleting a modifier not updating the items correctly? Why is this not refreshing the view? How can I resolve this issue?

Accepted Reply

Well, the issue seems to be that I'm not inserting the Modifier or Item into the context prior to creating a Transaction. I would only append the Modifier to the array in Item, and then I would append the Item to the array in Transaction. Once the Transaction was created, it would insert the Transaction into the context. I assumed that would insert all relevant models but I guess not.

I recall having an issue with this, because I don't want to insert the Modifier until the Transaction has been created. I believe I tried to use context.rollback() in a different application, however, it doesn't work because the Modifier is being inserted and saved. At the moment, this seems to be working for me.

First, I had to manually set a different modelContainer for the view that allows you to create a Transaction, and then disable auto-save.

.sheet(isPresented: $addTransaction, content: {
                TransactionEditor()
                    .modelContainer(for: Transaction.self, isAutosaveEnabled: false)
            })

I tested this, and it creates the models without any issues, but of course they don't get saved. So when you view ContentView(), nothing is there.

Second, in the my other editors (ItemEditor and ModifierEditor), I insert those models into the context before appending them to their specific arrays. Here is code from ModifierEditor(). This context of course being the one attached to TransactionEditor() since ModifierEditor() is in that same stack.

Button("Add") {
                    let modifier = Modifier(timestamp: timestamp, value: value)
                    context.insert(modifier)
                    modifiers.append(modifier)
                    dismiss()
                }

Then, in my TransactionEditor in the button that is creating the Transaction, I did this.

Section {
                    Button("Add") {
                        let transaction = Transaction(timestamp: timestamp, note: note, items: items)
                        context.insert(transaction)
                        do {
                            try context.save()
                        } catch {
                            print("Could not save transaction: \(error.localizedDescription)")
                        }
                        dismiss()
                    }
                }

That will save the transaction, and it successfully makes all relationships without having to close to reopen the application. I don't fully understand why I'm not able to only insert the Transaction. When I did it that way, it would make the relationship to the Item successfully, just not the Modifier to Item relationship. Anyways, I'm glad it seems to be working now.

Replies

I've found through further testing that the relationship between Modifier and Item is not being made until the app is closed and reopened. For example, lets say I create the Transaction, Item, and Modifier, then add the Modifier to the Item. When I get back to ContentView, that Modifier model does not contain anything inside of its items relationship. If I close and reopen the application, Modifier will then have a relationship with the Item is was added to. I can then delete the Modifier, and the counts are set to 0 as expected. I found this through using this function I created.

extension ModelContext {
    func deleteModifier(_ modifier: Modifier) {
        var itemsDescriptor = FetchDescriptor<Item>()
        var savedItems      = try? self.fetch(itemsDescriptor)
        
        if var savedItems {
            if let modifierItems = modifier.items {
                for item in modifierItems {
                    if let index = item.modifiers!.firstIndex(where: {$0.persistentModelID == modifier.persistentModelID}) {
                        savedItems.remove(at: index)
                    }
                }
            }
        }
        
        self.delete(modifier)
    }
}

I put stop points after if var savedItems and I could see that modifierItems count was 0 after the initial creation of the Transaction. After closing and reopening the app, modifierItems.count was 1 as expected.

Is there a way to force the relationship?

Well, the issue seems to be that I'm not inserting the Modifier or Item into the context prior to creating a Transaction. I would only append the Modifier to the array in Item, and then I would append the Item to the array in Transaction. Once the Transaction was created, it would insert the Transaction into the context. I assumed that would insert all relevant models but I guess not.

I recall having an issue with this, because I don't want to insert the Modifier until the Transaction has been created. I believe I tried to use context.rollback() in a different application, however, it doesn't work because the Modifier is being inserted and saved. At the moment, this seems to be working for me.

First, I had to manually set a different modelContainer for the view that allows you to create a Transaction, and then disable auto-save.

.sheet(isPresented: $addTransaction, content: {
                TransactionEditor()
                    .modelContainer(for: Transaction.self, isAutosaveEnabled: false)
            })

I tested this, and it creates the models without any issues, but of course they don't get saved. So when you view ContentView(), nothing is there.

Second, in the my other editors (ItemEditor and ModifierEditor), I insert those models into the context before appending them to their specific arrays. Here is code from ModifierEditor(). This context of course being the one attached to TransactionEditor() since ModifierEditor() is in that same stack.

Button("Add") {
                    let modifier = Modifier(timestamp: timestamp, value: value)
                    context.insert(modifier)
                    modifiers.append(modifier)
                    dismiss()
                }

Then, in my TransactionEditor in the button that is creating the Transaction, I did this.

Section {
                    Button("Add") {
                        let transaction = Transaction(timestamp: timestamp, note: note, items: items)
                        context.insert(transaction)
                        do {
                            try context.save()
                        } catch {
                            print("Could not save transaction: \(error.localizedDescription)")
                        }
                        dismiss()
                    }
                }

That will save the transaction, and it successfully makes all relationships without having to close to reopen the application. I don't fully understand why I'm not able to only insert the Transaction. When I did it that way, it would make the relationship to the Item successfully, just not the Modifier to Item relationship. Anyways, I'm glad it seems to be working now.