Reputation: 4300
I'm building a SwiftUI app with SwiftData where the start page is a list of Trip
objects and the DetailView
will be a map with Waypoint
objects that belong to that Trip
. This is a one Trip
to many Waypoints
relationship. I have simplified the code below - the detail page is a list with a button to delete a Waypoint
. In the real version, a user may tap on a Waypoint
and choose to delete it.
My code (the deleteOneWaypoint
func) breaks whenever the delete button is pushed just like in the real app when an annotation is tapped and selected for delete. Restarting the app shows that the Waypoint
is indeed deleted even though the code broke. The error message is in the expanded @Model
macro file at the line of the Waypoint
sequence var. See the error at the end of this post. I have been unable to figure out how to fix this.
This code will run as is for testing (import both SwiftUI and SwiftData)
ContentView:
struct ContentView: View {
@Environment(\.modelContext) private var context
@Query(sort: \.name, order: .forward) var trips: [Trip]
@State private var selectedTrip: Trip?
var body: some View {
NavigationSplitView {
List(selection: $selectedTrip) {
ForEach(trips, id: \.self) { trip in
VStack(alignment: .leading) {
Text(trip.name)
}
}
.onDelete{ indexSet in
indexSet.forEach { index in
context.delete(trips[index])
}
do {
try context.save()
} catch {
print(error)
}
}//on delete
}//list
.navigationTitle("Trips")
.padding()
} detail: {
//this ZStack seems to be needed to make iPad update correctly
ZStack {
if let selectedTrip {
DetailMapView(trip: selectedTrip)
} else {
Text("Select a Trip")
}
}
}//detail
.onAppear {
if trips.count == 0 {
context.insert(Trip.exampleTrip1)
context.insert(Trip.exampleTrip2)
}
}//on appear
}//body
}//struct
DetailView:
struct DetailMapView: View {
@Environment(\.modelContext) private var context
@Query(sort: \.name, order: .forward) var trips: [Trip]
@State private var sequenceToDelete: Int = 100
var trip: Trip
var body: some View {
VStack {
Text(trip.name)
if let waypoints = trip.waypoints {
List {
ForEach(waypoints, id: \.self) { wp in
VStack(alignment: .leading) {
Text("WP Sequence: \(wp.sequence)")
Text("WP Latitude: \(wp.latitude)")
Text("WP Longitude: \(wp.longitude)")
}
}
}
}
Button(action: {
deleteOneWaypoint(seq: 1)
}, label: {
Text("Delete WP with Sequence 1")
})
}//v
}//body
func deleteOneWaypoint(seq: Int) {
let identity = trip.objectID
//print("sequenceToDelete is \(sequenceToDelete)")
//print("seq is \(seq)")
if let waypoints = trip.waypoints {
for w in 0..<waypoints.count {
if waypoints[w].sequence == seq {
context.delete(waypoints[w])
}
}//for in
var newWaypoints: [Waypoint] = []
if let thisTrip: Trip = trips.first(where: { $0.objectID == identity} ) {
if let tWaypoints = thisTrip.waypoints {
for wp in 0..<tWaypoints.count {
print("thisTrip[wp]sequence \(tWaypoints[wp].sequence)")
}
newWaypoints = tWaypoints
}
}//if let thisTrip
for w in 0..<newWaypoints.count {
print("newWaypoints[w].sequence is \(newWaypoints[w].sequence)")
}
for w in 0..<newWaypoints.count {
if newWaypoints[w].sequence > seq {
newWaypoints[w].sequence = newWaypoints[w].sequence - 1
}
}//for in
//should not need this:
do {
try context.save()
} catch {
print(error)
}
}//f
}//delete one ...
}//struct
The Models:
@Model
final public class Trip: Identifiable {
var name: String = "no name"
@Relationship(.cascade) var waypoints: [Waypoint]?
init(name: String, waypoints: [Waypoint]? = nil) {
self.name = name
self.waypoints = waypoints
}
static let exampleTrip1 = Trip(name: "San Jose to Vallejo", waypoints: [
Waypoint(latitude: 37.338207, longitude: -121.886330, sequence: 0),
Waypoint(latitude: 37.548271, longitude: -121.988571, sequence: 1),
Waypoint(latitude: 38.1041, longitude: -122.2566, sequence: 2),
Waypoint(latitude: 37.4419, longitude: -122.1430, sequence: 3)
])
static let exampleTrip2 = Trip(name: "Palo Alto to Reno", waypoints: [
Waypoint(latitude: 37.4419, longitude: -122.1430, sequence: 0),
Waypoint(latitude: 38.5816, longitude: -121.4944, sequence: 1),
Waypoint(latitude: 39.5296, longitude: -119.8138, sequence: 2)
])
}//trip model class
@Model
final public class Waypoint: Identifiable, Comparable {
var latitude: Double = 0.0
var longitude: Double = 0.0
var sequence: Int = 0
init(latitude: Double, longitude: Double, sequence: Int) {
self.latitude = latitude
self.longitude = longitude
self.sequence = sequence
}
static let exampleWaypoint =
Waypoint(latitude: 40.760780, longitude: -111.891045, sequence: 1)
public static func <(lhs: Waypoint, rhs: Waypoint) -> Bool {
lhs.sequence < rhs.sequence
}
}//waypoint model
Error:
From expanded var sequence
:
var sequence: Int = 0 @_PersistedProperty
Thread 1: EXC_BAD_INSTRUCTION (code=EXC_I386_INVOP, subcode=0x0)
{init(newValue) accesses (_$backingData) { self.setValue(for: \.sequence, to: newValue)
} get {_$observationRegistrar.access(self, keyPath: \.sequence)
return self.getValue(for: \.sequence)//the ERROR is HERE
} set {_$observationRegistrar.withMutation(of: self, keyPath: \.sequence) {self.setValue(for: \.sequence, to: newValue)}
}
}
Any guidance would be appreciated. Xcode 15 beta 4, iOS 17
Upvotes: 0
Views: 741
Reputation: 114846
It seems that you need to explicitly remove the waypoint from the waypoints array as well as deleting it.
I am not sure if it is a bug or intended; I would have thought that the default behaviour of nullify
on the inverse relationship would have taken care of this.
For convenience I modified your Trip
so that waypoints
wasn't an optional and has a default empty array and replaced your button with an onDelete
:
struct DetailMapView: View {
@Environment(\.modelContext) private var context
@State private var sequenceToDelete: Int = 100
var trip: Trip
var body: some View {
VStack {
Text(trip.name)
List {
ForEach(trip.waypoints, id: \.self) { wp in
VStack(alignment: .leading) {
Text("WP Sequence: \(wp.sequence)")
Text("WP Latitude: \(wp.latitude)")
Text("WP Longitude: \(wp.longitude)")
}
}.onDelete{ indexSet in
indexSet.forEach { index in
let deletedWaypoint = self.trip.waypoints[index]
for waypoint in self.trip.waypoints {
if waypoint.sequence > deletedWaypoint.sequence {
waypoint.sequence -= 1
}
}
trip.waypoints.remove(at: index)
context.delete(deletedWaypoint)
}
do {
try context.save()
} catch {
print(error)
}
}
}
}//v
}//body
}//struct
With some modifications you can have your waypoints sorted by sequence:
struct DetailMapView: View {
@Environment(\.modelContext) private var context
@State private var sequenceToDelete: Int = 100
var trip: Trip
var sortedWaypoints: [Waypoint] {
return trip.waypoints.sorted { $0.sequence < $1.sequence }
}
var body: some View {
VStack {
Text(trip.name)
List {
ForEach(self.sortedWaypoints, id: \.self) { wp in
VStack(alignment: .leading) {
Text("WP Sequence: \(wp.sequence)")
Text("WP Latitude: \(wp.latitude)")
Text("WP Longitude: \(wp.longitude)")
}
}.onDelete{ indexSet in
indexSet.forEach { index in
let deletedWaypoint = self.sortedWaypoints[index]
var filteredWaypoints = [Waypoint]()
for waypoint in self.trip.waypoints {
if waypoint.sequence != deletedWaypoint.sequence {
filteredWaypoints.append(waypoint)
}
if waypoint.sequence > deletedWaypoint.sequence {
waypoint.sequence -= 1
}
}
context.delete(deletedWaypoint)
trip.waypoints = filteredWaypoints
}
do {
try context.save()
} catch {
print(error)
}
}
}
}//v
}//body
}//struct
Upvotes: 2