TEAM LICENSES: Save money and learn new skills through a Hacking with Swift+ team license >>

SOLVED: Hacking with SwiftData - Delete a sight

Forums > Books

It seems like a silly thing, but deleting a sight is just giving me issues. It does not seem like it should be an issue, but deleting a sight is causing crashes when you navigate in and out of an editDestinationView, so I suspect that I am missing something fairly obvious.

import SwiftUI
import SwiftData

struct EditDestinationView: View {

    @Bindable var destination: Destination

    @State private var newSightName = ""

    @Environment(\.modelContext) var modelContext

    var body: some View {
        Form{
            TextField("Name", text: $destination.name)
            TextField("Details", text: $destination.details)
            DatePicker("Date", selection: $destination.date)

            Section("Priority"){
                Picker("Priority", selection: $destination.priority){
                    Text("Meh.").tag(1)
                    Text("Maybe").tag(2)
                    Text("Must").tag(3)
                }
                .pickerStyle(.segmented)
            }
            Section("Sights"){
                ForEach(destination.sights) { sight in
                    Text(sight.name)
                }
                .onDelete(perform: deleteSights)

                HStack{
                    TextField("Add a new sight in \(destination.name)", text: $newSightName)
                    Button("Add", action: addSight)
                }
            }
        }
        .navigationTitle("Edit Destination")
        .navigationBarTitleDisplayMode(.inline)
    }

    func addSight(){
        guard newSightName.isEmpty == false else {return}

        withAnimation {
            let sight = Sight(name: newSightName)
            destination.sights.append(sight)
            newSightName = ""
        }
    }

 //   For some reason this kind of works.... does not seem like it should
    //Yeah this method is not working, it leaves the sight objects in the data base and only deletes the destinations reference to them really have to have the moc included to delete anything from the database
//    func deleteSights(_ indexSet: IndexSet){
//        destination.sights.remove(atOffsets: indexSet)
//    }

    func deleteSights(_ indexSet: IndexSet){
        for index in indexSet{
            let sight = destination.sights[index]
            modelContext.delete(sight)
        }
    }
}

#Preview {
    do {
        let config = ModelConfiguration(isStoredInMemoryOnly: true)
        let container = try ModelContainer( for: Destination.self, configurations: config)

        let example = Destination(name: "Example Destination", details: "Example details go here and will automatically expand as they are edited.")
        return EditDestinationView(destination: example)
            .modelContainer(container)
    } catch {
        fatalError("Failed to create model container.")
    }
}

When you delete a sight and then try to go back into the Destination in question it crashes immediatly. Has anyone gotten this to work without crashing when you try to show the view again?

I also had an issue with getting the navigationDestination working as soon as you tried to add the @Environment to EditDestinationView, that was solved by changing the navigationDestination modifier to this format.

.navigationDestination(for: Destination.self){ destination in
                    EditDestinationView(destination: destination)
                }

Note: Minor edit to the initial code to remove a confounder in the form of a Query to the sights, just dont do that. Go through the desintation.

3      

I’m having exactly the same issue.

I fixed the @Envrionment issue by making the modelContext variable private. Plus I refrenced the Sight object via the destinations rather than using a new @Query.

let sight = destination.sights[index]

I did the following troubleshooting.

BEFORE DELETE

sqlite> select * from zdestination where z_pk = 35;

Z_PK Z_ENT Z_OPT ZPRIORITY ZDATE ZDETAILS ZNAME
35 1 542 3 723514860 More details to come, mostly because I’m bored and couldn’t Land of The Happy Trees
be bothered typing anything in just now, although, it seams I
have just typed a lot about nothing. Although there could b
e something to this nothing, it is after all a fictitious pl
ace with happy trees. Odd, maybe everyone smokes dope and it
s not the trees that are happy at all. Was there something a
bout a talking cat?

sqlite> select * from zsight where z1sights = 35;

Z_PK Z_ENT Z_OPT Z1SIGHTS ZNAME
27 2 1 35 New Destination - Sight 1
28 2 1 35 New Destination - Sight 2
35 2 1 35 Another Sight

AFTER DELETE

Data after deleting “Another Sight”. Still on the same view, just swiped to delete the sight. The database has already been updated to reflect the deletion.

sqlite> select * from zsight where z1sights = 35;

Z_PK Z_ENT Z_OPT Z1SIGHTS ZNAME
27 2 1 35 New Destination - Sight 1
28 2 1 35 New Destination - Sight 2

If you try and modify anything about “Land of The Happy Trees” again the app crashes. Even if you go out of this view and come back, BAM! Crashes.

There seams to be an in memory refrence to the deleted object???

It is really staring to look like a SwiftUI issue rather than a SwiftData issue, as the database is reflecting the changes instantly.

Plus, if you restart the app after the deletion, you can access “Land of The Happy Trees” without issue.

There seams to have been an issue with SwiftUI and ForEach view during the beta program. Apple recommended doing an explicit save after the deletion, I tried and this did not fix the issue.

https://developer.apple.com/documentation/xcode-release-notes/xcode-15-release-notes#Resolved-Issues. Look under Swift Data, Resolved Issues.

I used Paul’s handle little extension to get the path of the store and use sqlite3. Great idea. Helped allot plus it is actually good to see the data directly, feels more tangable.

https://www.hackingwithswift.com/quick-start/swiftdata/how-to-read-the-contents-of-a-swiftdata-database-store

I’m stumped form here, but I’ll keep trying.

6      

Hello Frinds, thanks @KitchenCrazy for pointing out making the modelContext private, that worked for the deleting sight, but i had another issue which is fatal error after deleting the las sight in the Sights section, but it seemed to be working after this change to the windowGroup.

import SwiftUI
import SwiftData

@main
struct iTourApp: App {

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
        .modelContainer(for: [Destination.self, Sight.self])
    }
}

Still not sure thou, I'll keep watching here if anybody has a better sultion, looking forward for your feedback

3      

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Great suggestion @abedalAhmad. That unfortunately didn't work for me, but was worth a try still.

From my understanding too, when there is a relationship between two model classes, inferred or otherwise, as there is in Destination by using Sight in an array plus the cascade delete relationship, ModelContainer will automatically include it behind the scenes, like you have.

I'm heavily leaning toward this being a SwiftUI issue.. it sure is an interesting one.

5      

@Raskals, did the trick for me. Thanks.

Performing an explicit save seamed to persist the changes instantly, I was able to quit the app real quick then when i realoaded it did not come back.

func deleteSight(_ indexSet: IndexSet) {
    for index in indexSet {
        destination.sights.remove(at: index)
        try? modelContext.save()
    }
}

I also did both methods to remove the site from the array and the context:

func deleteSight(_ indexSet: IndexSet) {
    for index in indexSet {
        let sight = destination.sights[index]
        modelContext.delete(sight)
        destination.sights.remove(at: index)
    }
}

That seamed to work fine too, for me anyway.

4      

I am so glad to see that I am not alone on this one. @KitchenCrazy, that is a great breakdown on what I am seeing as the problem.

To keep track of things that are still in the database I stuck a @Query for all sights on the ContentView.

An interesting thing happens when using this method.

func deleteSight(_ indexSet: IndexSet) {
    for index in indexSet {
        let sight = destination.sights[index]
        modelContext.delete(sight)
        destination.sights.remove(at: index)
    }
}

If you have multiple sights in a single destination, this method will delete two sights, deleting the first one from the database, and breaking the reference to another sight on the destination object, leaving it in the database.

The interesting thing is that for some reason this stops the crash. So I am led to believe that your assertion is correct and that there is a lingering reference to a sight object that no longer exists when it is crashing.

With this in mind, I thought, perhaps just touching the destination.sights would force an update and clear up the issue, and it totally does.... sooo yep.

func deleteSights(_ indexSet: IndexSet){
        for index in indexSet{
            let sight = destination.sights[index]
            modelContext.delete(sight)
            destination.sights.removeAll { $0.name == ""}
        }
    }

So this is the solution/workaround that seems to work, forcing an update somewhere in the memory to make sure that the bad reference is gone. Doing a removeall "" because we will never have a blank for the name given how the program is setup. There might be a better way to do this, but its what I got to work quickly.

Now of course this is kinda wonky, there might be a better way to accomplish this, but this really does take care of the crash.

4      

@Raskals Unfortunatly that function will wind up orphaning the sight object that you attempt to delete from the database, it only removes the reference to it on the destination, not the actual sight object.

It looks like it works, but it is only a partial solution, and I suppose if you were working with a larger program that was dealing with many sight objects you would run into storage issues eventually.

@KitchenCrazy Your first points in your first post are VERY important. Setting the @Environment to private is a great idea that fixes my very first frustration with this challenge and eliminates the need to make the navigationDestination modifier more verbose.

And avoiding the @Query for sights is also very important, because obviously going through that query just messes things up the more destinations and sights you might have, and it causes no end of issues with deleting the wrong sight when other things are working.

Great stuff all, I am happy to have found an apparent work around, but boy do I not like it. As KitchenCrazy said, this looks a lot like a swiftui error. I have to admit that I might still be missing something in my logic, but given the nature of the solution it seems unlikely.

4      

Hi all,

I'm also struggling to solve the 'delete a sight' challenge.

I discovered Apple has a sample SwiftData project, and after digging around it in Xcode discovered that it deals with this exact scenario, albeit through a bit more complex project (thank you @twostraws for giving us a more manageable intro).

https://developer.apple.com/documentation/coredata/adopting_swiftdata_for_a_core_data_app

Annoyingly, I'm not able to get the app running in the simulator...I suspect I'm not initializing the app bundle identifier correctly (the ReadMe gives set up details). But if you navigate to the BucketListView view, you'll find what looks to be the exact type of scenario given in this challenge.

I played around with their variation on .onDelete without success - hoping someone here is a little smarter than myself about it and can report back!

3      

hi,

i think the code for deleting a sight is OK as it stands:

    func deleteSights(indices: IndexSet) {
        for index in indices {
            let sight = destination.sights[index]
            modelContext.delete(sight)
        }
    }

yes, there's a crash in your future that's being reported above, but i added this definition to the Sight model:

    @Relationship(deleteRule: .nullify, inverse: \Destination.sights)
    var destination: Destination?

this explicitly says to remove the sight's reference in the owning destination's array of sights with .nullify, and it seems to work for me. i no longer see a crash.

my guess: without explicity defining the back-reference, SwiftData probably synthesizes it for you, and maybe it's not doing a real good job of it.

hope that helps,

DMG (not an AI-generated response)

7      

@delawaremathguy

That is a fantastic solution to clear up the problem without resorting to my .removeAll "" trick to get the system to delete the reference, and it puts the code in a much more reasonable spot, I love it and have changed the solved flag to your solution.

Thanks for responding, I kept checking back to see if anyone came up with a better solution to what certainly appears to be a bug in the api.

3      

@delawaremathguy Thanks for your solution!

I'm not working with this sample project, but I encountered the same basic issue with a crash on delete on my own project, and creating that inverse relationship manually (a la Core Data) was exactly what I needed to fix the problem. :)

1      

@delawaremathguy

I made an account just to thank you for your solution. Thank you! It works brilliantly.

— Quint

(also not an AI. Just normal I, or lack thereof...)

   

Hacking with Swift is sponsored by RevenueCat.

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.