JohnSF
JohnSF

Reputation: 4300

Cannot Delete an Object of One to Many Relationship Programmatically

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

Answers (1)

Paulw11
Paulw11

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

Related Questions