BLACK FRIDAY SALE: Save big on all my Swift books and bundles! >>

Day 59, Deleting Object from Core Data crushes the App

Forums > 100 Days of SwiftUI

Hi! I'm on day 59 and currently work with the filtered list that I've created in Day 58, part 2.

I did everything according to the tutorial but also decided to implement deletion and that's the part where I encountered a problem. There are 3 singers. I use a predicate "A" to show singers whose last name starts with the letter A and "S" for those whose last name starts with the letter "S". So the singers on the screen, I choose predicate "S", and the list shows me 2 objects. I delete them. no problem at all. After that, I choose the predicate "A" and the last singer is presented now. When I delete this singer - another one (like a ghost) appears. I don't know how Swift gives it a name and last name, because when I check it with print( "\(singer.wrappedName) \(singer.wrappedLastName)") I get printed "Unknown name Unknown last name", but on my simulator screen Adele Adkins is shown. If I try to delete it again - app crushes, because I try to delete the object that doesn't exist.

I've got 2 swift files: ContentView:

import SwiftUI
import CoreData

struct ContentView: View {
    @Environment(\.managedObjectContext) private var viewContext

    @State var lastNameFilter = "A"
    @State var ascendingOrder = true
    @State var sortByNameOrLastName: KeyPath = \Singer.lastName

    var body: some View {
        NavigationView {
            VStack {
                ListOfSingers(filterKey: "lastName", filterValue: lastNameFilter, sortingKey: sortByNameOrLastName, ascending: ascendingOrder) { (singer: Singer) in
                    Text("\(singer.wrappedName) \(singer.wrappedLastName)")
                        .onAppear { print( "\(singer.wrappedName) \(singer.wrappedLastName)" )}
                }
                Button("Add Singers") {
                    let singer1 = Singer(context: viewContext)
                    singer1.firstName = "Taylor"
                    singer1.lastName = "Swift"

                    let singer2 = Singer(context: viewContext)
                    singer2.firstName = "Ed"
                    singer2.lastName = "Sheeran"

                    let singer3 = Singer(context: viewContext)
                    singer3.firstName = "Adele"
                    singer3.lastName = "Adkins"

                    try? viewContext.save()
                }
                .frame(width: 280, height: 50)
                .background(Color.blue)
                .foregroundColor(.white)
                .cornerRadius(10)
                .padding()

                HStack {
                    Button("Sort by A") {
                        withAnimation {
                            lastNameFilter = "A"
                        }
                    }
                    .frame(width: 130, height: 50)
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(10)

                    Spacer()

                    Button("Sort by S") {
                        withAnimation {
                            lastNameFilter = "S"
                        }
                    }
                    .frame(width: 130, height: 50)
                    .background(Color.blue)
                    .foregroundColor(.white)
                    .cornerRadius(10)
                }
                .padding()
            }
            .navigationTitle("Singers")
            .toolbar {
                ToolbarItem(placement: .navigationBarLeading) {
                    Button("Sort by name") {
                        withAnimation {
                            if sortByNameOrLastName == \Singer.lastName {
                                sortByNameOrLastName = \Singer.firstName
                            } else {
                            sortByNameOrLastName = \Singer.lastName
                            }
                        }
                    }
                }
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button("Change Order") {
                        withAnimation {
                            ascendingOrder.toggle()
                        }
                    }
                }
            }
        }

    }

And ListOfSingers:

import CoreData
import SwiftUI

struct ListOfSingers<T: NSManagedObject, Content: View>: View {
    @Environment(\.managedObjectContext) private var viewContext

    @FetchRequest<T> var singers: FetchedResults<T> 

    let content: (T) -> Content
    init(filterKey: String, filterValue: String, sortingKey: KeyPath<T, String?>, ascending: Bool, @ViewBuilder content: @escaping (T) -> Content) {
        _singers = FetchRequest<T>(entity: T.entity(), sortDescriptors: [NSSortDescriptor(keyPath: sortingKey, ascending: ascending)], predicate: NSPredicate(format: "%K BEGINSWITH %@", filterKey, filterValue))
        self.content = content
    }

    var body: some View {

        List {
            ForEach(singers, id: \.self) {
                content($0)
            }.onDelete(perform: deleteSinger)
        }

    }

    func deleteSinger(at offsets: IndexSet) {
        withAnimation {
            print("BEFORE deletion: \(singers.count)")
            offsets.map { singers[$0] }.forEach(viewContext.delete)

            try? viewContext.save()
            print("AFTER deletion: \(singers.count)")
        }
    }
}

I don't even understand why is it happening. I check the number of elements in the array after deletion and it says 0. If it's 0, how the singer is shown in the List if it doesn't exist in the array that is used to populate the List <emoji head explosion> Thanks for your help guys!

1      

Change your delete function to this and have a try:

func deleteSinger(at offsets: IndexSet) {

       for  offset in offsets{
       viewContext.remove(at:offset)
       }

        try? viewContext.save()
        print("AFTER deletion: \(singers.count)")
    }
}

1      

remove(at:offset) is a method to delete items from an array. View Context aka managedObjectContext doesn't have it. It has a method func delete(_ object: NSManagedObject) which deletes object from the Persistent Container. My offsets.map { singers[$0] }.forEach(viewContext.delete) finds the object and sends it to viewContext for deletion. But for some reason it doesn't happen is the object is the last one.

1      

I came back here after more than a month a came up with this:

The problem was that for some reason ListOfSingers struct was called twice. Even though I've deleted the last singer from Core Data the render time was faster than reindexing of Core Data. So I ended up having a "Ghost View", which was rendered before I item was fully deleted from the Core Data. When I tried to delete a Ghost View I was reaching an array that didn't exist. It always lead to an app crash.

The solution was to add a function that checked if there are any items in Core Data right after deletion fired off.

1) Create @State var noMoreSingers = false in the main view to check which view should be presented and add this conditional to the VStack to explicitly tell which view should be presented:

 VStack {
     if !noMoreSingers {
         ListOfSingers(filterKey: "lastName", filterValue: lastNameFilter) { (singer: Singer) in
             Text("\(singer.wrappedName) \(singer.wrappedLastName)")
         }  } else {
             Spacer()
         }

2) Add a @Binding var noMoreSingers: Bool to ListOfSingers to be able to tell the main view when there are no more items in Core Data

3) Add function to Persistanse Controller to get all items:

     func getAllSingers() -> [Singer] {
     let request: NSFetchRequest<Singer> = Singer.fetchRequest()

     do {
         return try container.viewContext.fetch(request)
     } catch {
         fatalError("Error Fetching Users")
     }
 }

4) Add function to ListOfSingers for fire off getAllSingers and check if the retrieved array is empty:

func checkIfSingersAreEmpty() {
     let checkSingers = PersistenceController.shared.getAllSingers()
     if checkSingers.isEmpty {
         noMoreSingers = true
     }
 }

Now everything works!

1      

Hacking with Swift is sponsored by RevenueCat

SPONSORED In-app subscriptions are a pain to implement, hard to test, and full of edge cases. RevenueCat makes it straightforward and reliable so you can get back to building your app. Oh, and it's free if your app makes less than $10k/mo.

Learn more

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

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

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.