Nigel-W
Nigel-W

Reputation: 63

How do I delete child items from list with SwiftData?

I'm new to SwiftUI and for my first app I decided to try SwiftData since I don't want to have to convert it to SwiftData later. My app has timers (MyTimer) and each timer has a name and several resets which each have a date and a text string. I want the user to be able to select a MyTimer and view the list of its resets then from there be able to edit or delete any of them. So my first view allows the creation of a MyTimer and to add resets to it. The second view shows a list of the resets for the MyTimer selected.

The problem I have is when I delete more than one reset, the array of resets doesn't seem to update and if you delete a second reset from the same line of the list it tries to delete a reset which has already been deleted and I get the error: Thread 1: Fatal error: This method isn't prepared to operate on a backing data that's unmoored from its managed object context. Relationships will need to be deep copied by objectID for that to work.

This is the code for the app Data Model file:

import Foundation
import SwiftData
import SwiftUI

@Model
final class MyTimer {
    var id: String
    var name: String
    
    @Relationship(deleteRule: .cascade)
    var resets: [Reset]
    
    @Transient
    var sortedResets: [Reset] {
        print(resets.count)
        return self.resets.sorted { $0.date > $1.date }
    }

    init(_ name: String = "",
         startDate: Date = .now) {

        let id = UUID().uuidString
        self.id = id
        self.name = name
        self.resets = [Reset(text: "Started On", date: startDate)]
    }
}

@Model
class Reset: Identifiable {
    var id: String
    var text: String
    var date: Date

    init(text: String, date: Date) {
        self.id = UUID().uuidString
        self.text = text
        self.date = date
    }
}

App File:

import SwiftUI
import SwiftData

@main
struct SwiftDataHabitTestApp: App {

    let container = try? ModelContainer(for: MyTimer.self, Reset.self)


    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(container!)
    }
}

ContentView:

import SwiftUI
import SwiftData

struct ContentView: View {

    @Environment(\.modelContext) private var modelContext
    @State var timerName: String = ""
    @State var resetViewShowing = false
    @State var timerForResetView: MyTimer?
    @Query(sort: \MyTimer.name) var allTimers: [MyTimer]

    var body: some View {
        ZStack {
            VStack {
                TextField(text: $timerName, prompt: Text("Name your Timer")) {
                    Text("Name:")
                }
                Button {
                    let newTimer = MyTimer(timerName)
                    modelContext.insert(newTimer)
                } label: {
                    Text("Create Timer")
                }
                List {
                    ForEach(allTimers){ dispTimer in
                        Text(dispTimer.name)
                        Button {
                            let newReset = Reset(text: "New REset", date: .now)
                            dispTimer.resets.append(newReset)
                            try? modelContext.save()
                        } label: {
                            Text("Reset")
                        }
                        Button {
                            resetViewShowing = true
                            timerForResetView = dispTimer
                        } label: {
                            Text("View Resets")
                        }

                    }
                    .onDelete(perform: { indexSet in
                        indexSet.forEach { index in
                            modelContext.delete(allTimers[index])
                        }
                    })
                }

            }
            .padding()
            if resetViewShowing {
                ResetListView(dispTimer: timerForResetView!)
            }
        }
    }
}

Reset List View

import SwiftUI
import SwiftData

struct ResetListView: View {

    @Environment(\.modelContext) private var modelContext
    @State var dispTimer: MyTimer

    var body: some View {
//  var resetList: [Reset] = dispTimer.resets.sorted { $0.date > $1.date }
        List {
            ForEach(dispTimer.resets) {
                Text($0.text)
            }
            .onDelete(perform: { indexSet in
                indexSet.forEach { index in
                    print("index: \(index)")
                    print("length of resets: \(dispTimer.resets.count)")
                    modelContext.delete(dispTimer.resets[index])
                    do {
                        try modelContext.save() <<---- Error: Thread 1: Fatal error: This method isn't prepared to operate on a backing data that's unmoored from its managed object context. Relationships will need to be deep copied by objectID for that to work.
                    } catch {
                        print("error saving")
                    }
                }
            })
        }
    }
}

I've tried several ways of using a sorted array of resets as I would like this to display in the list in order by date, sorting them into a var in the view or using a computed property in MyTimer but then I get a different error: Thread 1: EXC_BREAKPOINT (code=1, subcode=0x1a8a4fad8) which appears in the getter for reset's date property I've also tried instead of using .ondelete to use a .swipeActions but that doesn't seem to solve the problem.

Upvotes: 6

Views: 1834

Answers (1)

Paulw11
Paulw11

Reputation: 114846

There is a hint to your issue in the error message -

This method isn't prepared to operate on a backing data that's unmoored from its managed object context. Relationships will need to be deep copied by objectID for that to work

So, how do we get that "real" object?

You can use its persistentIdentifier to fetch it from the managed object context.

Note that as well as deleting the object you need to explicitly remove it from the resets array in the MyTimer object.

Changing your delete code to :

for index in IndexSet {
    let objectId = timer.resets[index].persistentModelID
    let reset = modelContext.model(for: objectId)
    modelContext.delete(reset)
}
timer.resets.remove(atOffsets: offsets)
do {
    try modelContext.save()
} catch {
    print("Error saving context \(error)")
}

will fix your problem.

Upvotes: 7

Related Questions