Reputation: 63
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
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