Search code examples
iosswiftswiftdata

SwiftData does not cascade delete relationship entries


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.

enter image description here


Solution

  • 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 Lists 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.