I'm trying to delete a parent along with its array of children. What happens is that the parent is deleted, but not the children in the array. The child just has its parent relationship set to null. If I change the deleteRule
to cascade
on the child and delete it, then the parent is deleted. So I'm a bit confused why it doesn't work the other way around.
Xcode 15.0.1 15A507 iOS 17.0 iPhone 15 Pro Simulator
Model.swift
import Foundation
import SwiftData
import SwiftUI
protocol Storeable {
var timestamp: Date { get }
}
@Model
final class Child: Storeable {
var timestamp: Date
@Relationship(deleteRule: .nullify) var parent: Parent?
init(timestamp: Date) {
self.timestamp = timestamp
}
}
@Model
final class Parent: Storeable {
var timestamp: Date
@Relationship(deleteRule: .cascade, inverse: \Child.parent)
var items: [Child]?
init(timestamp: Date) {
self.timestamp = timestamp
}
}
final class StoreableItem: Storeable, Identifiable {
var id: UUID
var timestamp: Date
var itemType: ItemType
init<U>(storeable item: U) where U: Storeable {
self.timestamp = item.timestamp
self.id = UUID()
switch item {
case let i as Child: itemType = .child(i)
case let m as Parent: itemType = .parent(m)
default: itemType = .unknown
}
}
}
enum ItemType {
case child(Child)
case parent(Parent)
case unknown
}
AppFile.swift
import SwiftUI
import SwiftData
@main
struct stackoverflowApp: App {
var sharedModelContainer: ModelContainer = {
let schema = Schema([
Parent.self, Child.self
])
let modelConfiguration = ModelConfiguration(schema: schema, isStoredInMemoryOnly: false)
do {
return try ModelContainer(for: schema, configurations: [modelConfiguration])
} catch {
fatalError("Could not create ModelContainer: \(error)")
}
}()
var body: some Scene {
WindowGroup {
ContentView()
}
.modelContainer(sharedModelContainer)
}
}
ContentView
import SwiftUI
import SwiftData
struct ContentView: View {
@Environment(\.modelContext) private var modelContext
@Query private var Childs: [Child]
@Query private var Parents: [Parent]
var allItems: [StoreableItem] {
let mappedChilds = Childs.filter { $0.parent == nil }.map { StoreableItem(storeable: $0) }
let mappedParents = Parents.map { StoreableItem(storeable: $0) }
return mappedChilds + mappedParents
}
var body: some View {
NavigationStack {
List {
ForEach(allItems) { item in
switch item.itemType {
case .child(let child):
Text("Child: \(child.timestamp.formatted())")
.swipeActions {
Button("Delete", role: .destructive) {
modelContext.delete(child)
try? modelContext.save()
}
}
case .parent(let parent):
Text("Parent: \(parent.timestamp.formatted())")
.swipeActions {
Button("Delete", role: .destructive) {
modelContext.delete(parent)
try? modelContext.save()
}
}
default: EmptyView()
}
}
}
.listStyle(.plain)
.toolbar {
Button(action: add) {
Label("Add", systemImage: "plus")
}
}
}
}
private func add() {
withAnimation {
let Parent = Parent(timestamp: Date())
modelContext.insert(Parent)
let child = Child(timestamp: Date())
child.parent = Parent
modelContext.insert(child)
}
}
}
SortableItem
is a type erasing wrapper for the objects so I can render them in a list together.
You have a couple of problems; The first is the way you are putting two different object types into the allItems
array - Because that array isn't observed by SwiftUI in any way, you will get strange and inconsistent updates. For example, when I ran your code I saw parents appearing but not children (even though the child objects were being created as I could see when I added separate lists that use the @Query
arrays directly).
But that isn't what is causing your deletion problem. Even with separate List
s I could see that the Child
objects were not being deleted.
Switching from a .swipeAction
to onDelete
made the deletion work correctly -
List {
ForEach(allItems) { item in
switch item.itemType {
case .child(let child):
Text("Child: \(child.timestamp.formatted())")
case .parent(let parent):
Text("Parent: \(parent.timestamp.formatted())")
default: EmptyView()
}
}.onDelete(perform: { indexSet in
for index in indexSet {
let item = allItems[index]
switch item.itemType {
case .child(let child):
modelContext.delete(child)
case .parent(let parent):
modelContext.delete(parent)
default:
break
}
}
})
}
Now, I can't tell you why this is. In theory using a swipe action and deleting the object directly should work.