GO FURTHER, FASTER: Try the Swift Career Accelerator today! >>

SOLVED: Updating displayed CoreData after deletion

Forums > SwiftUI

I use an split view on my Mac app that shows a list of students (left) and a detailedView of each of them (right). When I delete one student from left (detailed) view, it doesn't dissapear from both detailedView nor general list until I load another view and come back again.So changes are saved to CoreData correctly, but not updated in memory.

I have found some information about "merging" in-memory data and CoreData-updated data, but always refered to batch deletion, not just for one item. My delete function is defined as easy as:

func delete() {
    let moc = self.managedObjectContext
    moc?.delete(self)
    try? moc?.save()    
  }

How could I add some lines of code in order to merge/update displayed data?

2      

@Bnerd  

Your student Entity that is loaded in your views should be @Observedobject, I don't have split views, but whenever I save my CoreData for Entitities that are marked @Observedobject the views update.

2      

Thanks @Bnerd. It should, but it does not. And I use @ObservedObject on all of them.

In fact, it is not a split view, but a Navigation View. When deleting student on iOS target, I get this "animation":

https://www.youtube.com/shorts/YJbkZckfdMs

But on MacOS, same code, doesn't update any of both views: neither detailed one nor list one:

https://www.youtube.com/watch?v=KUfJT21EYTc&feature=youtu.be

Any further ideas?

2      

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 and A/B test your entire paywall UI without any code changes or app updates.

Learn more here

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

@Bnerd  

If you can share the code for your list maybe we will get some ideas. In the IOS the View updates probably because you move between list to detail, but on the MACOS it seems that the element remains although it's information becomes nil.

2      

The relevant code on each view is as follows.

From studentsView (general list, parent):

struct StudentsView: View {
  @Environment(\.managedObjectContext) var moc
  @FetchRequest(sortDescriptors: []) var students: FetchedResults<Student>

  //

  var body: some View {
    List {
    ForEach(filteredStudents, id: \.id) { student in
        NavigationLink(destination: StudentDetailView(student: student)) {
          StudentCellView(student: student)
        }

From studentCellView (not so relevant, only the refrence for the list):

struct StudentCellView: View {

  @Environment(\.managedObjectContext) var moc
  @ObservedObject var student: Student

  //...
  var body: some View {
  //...

From studentDetailView:

struct StudentDetailView: View {

  @Environment(\.managedObjectContext) var moc
  @ObservedObject var student: Student

  //...

  var body: some View {
    Form {
    //...

From studentEditView:

struct StudentEditView: View {

  @Environment(\.managedObjectContext) var moc
  @Environment(\.dismiss) private var dismiss

  @ObservedObject var student: Student
  //...
  var body: some View {

    NavigationView {
       VStack {

       //...
       Section {
          Button("Delete student") {
            isShowingDeletionDialog = true
          }

          //...
          .confirmationDialog(studentRehearsals == 0 ? "¿Are you sure you want to delete this student?" : "¿Are you sure you want to delete this student? \(studentRehearsals) related rehearsal(s), will be deleted too.", isPresented: $isShowingDeletionDialog, titleVisibility: .visible) {
        Button(role: .destructive){
          student.delete()
          dismiss()
        } label: {
          Text("Confirm")
        }
        Button("Cancel", role: .cancel) { }
      }

On CoreData model:

extension Student {

func delete() {
    let moc = self.managedObjectContext
    moc?.delete(self)
    try? moc?.save()    
  }

2      

@Bnerd  

What about your "filteredStudents" ?

2      

filteredStudents is an array to contain fetched content from CoreData for the view:

@State private var filteredStudents = [Student]()

As soon as the view appears, it fills. I use the same function to update data based on searchings:

.onAppear {
      checkData(order: orderedBy)
    }

//...

    func checkData(order: String){

    filteredStudents = studentSearch(query: query)

    if markedFilter {
      filteredStudents = filteredStudents.filter { $0.marked == markedFilter }
    }

    switch order {
    case "student" :
      filteredStudents = filteredStudents.sorted(by: { ($0.name?.strippingDiacriticsLowercased ?? "") < ($1.name?.strippingDiacriticsLowercased ?? "") })
    case "instrument" :
      filteredStudents = filteredStudents.sorted(by: {
        ($0.relatedInstrument?.name?.strippingDiacriticsLowercased ?? "") < ($1.relatedInstrument?.name?.strippingDiacriticsLowercased ?? "") })
    case "course" :
      filteredStudents = filteredStudents.sorted(by: {
        ($0.relatedCourse?.level?.strippingDiacriticsLowercased ?? "") < ($1.relatedCourse?.level?.strippingDiacriticsLowercased ?? "") })
    case "tutor" :
      filteredStudents = filteredStudents.sorted(by: {
        ($0.relatedTutor?.name?.strippingDiacriticsLowercased ?? "") < ($1.relatedTutor?.name?.strippingDiacriticsLowercased ?? "") })
    default : break
    }
  }

I'm almost sure that it is related to the way in-memory data merges with persistent data, as explained here: https://stackoverflow.com/questions/60230251/swiftui-list-does-not-update-automatically-after-deleting-all-core-data-entity

But all information I find is related to bulk deletion, and NSBatchDeleteRequests, and this is just deleting one unique object.

2      

try appending to func delete() a call to checkData() so as to update the @State var filteredStudents

2      

I was trying that this afternoon. But I can't find how, as

func checkData(order: String){ }

is defined on StudentsView (parent list view), and a parameter "order" (an String defining students sorting) is requiered. I don't have that data on studentEditView, so how can I add a call to this function? I tried with a public "external" function too, but I still need the parameters...

2      

Then pass order as a parameter and filteredStudents as a binding to StudentEditView. You add a parameter to a view by defining it as a var without a property wrapper:

var order: String
@Binding var filteredStudents

order also needs to be an @State var in the parent view that defines it.

You may have to re-think where the @State vars are defined to be sure you can pass them down to child views that reference them.

2      

Try to rethink your structure. Everytime I run into such a stopping point I do that.

F.e. you could use a class DataController as EnvironmentObject which does all the deleting and fetching for you. You don't have to put that all in your Student class.

2      

@Bnerd  

Ok, as I expected, your "filteredStudents" is the @State property that controls your view, but does not understand if the Entity Student has updates i.e. You deleted a student. If you want to keep the current code try this : In your StudentsView add this

@State private var isAStudentDeleted = false

In the child views add a Binding

@Binding var isAStudentDeleted: Bool

Place in your relevant functions:

isAStudentDeleted.toggle

Finally at your StudentsView add this below your .onAppear

.onChange(of: isAStudentDeleted, perform: { _ in
            checkData(order: orderedBy)
        })

You should be ok now ;)

2      

Thanks you all very much.

  • About @Bnerd approach:
  1. I add @State property and .onChange modifier to my "studentView". OK.
  2. I add @Binding property to "studentEditView" and change its value to true just after deleting. BUT the compiler fails with no clues ("The compiler is unable to type-check this expression in reasonable time; try breaking up the expression into distinct sub-expressions"). I think because "studentDetailView", the intermediate view wich trigers "studentEditView" has no reference neither to this new @State nor @Binding property.

I've tried even with @AppStorage vars in order to use an app-wide variable to rely on to force updating, but it doesn't change its value on "studentsView" when I change it on "studentEditView". I don't know why.

Trying the other options suggested by @Hatsushira and @bobstern, although them forces to rebuild a great part of the app :-(

The most frustrating thing is that my app has been working perfectly well on iOS, but having three NavigationView-relationed views visible on MacOS at the same time makes it inoperative for such a tiny reason.

I tried as well not making use of an intermediate "filteredStudents" array, because I know it is the origin of the problem, as @Bnerd suggest, but I can modify sorting and reordering directly from fectRequestData, as doing so:

func checkData(order: String){

    students = studentSearch(query: query)

throws error: "Cannot assign to property: 'students' is a get-only property". Doing this way, both studentsView and studentDetailView, connected, updates like a charm.

So... I reformulate my problem: how could I re-write checkData method in order to operate directly with this fetchedData?

2      

@Bnerd  

Do you have an Observable model that you could pass it along as an EnvironmentObject? If yes, put the boolean there as an @Published var If not add also the binding var to your StudentDetailView. It should work, the Edit updates the binding var, this updates the binding in the StudentDetailView, which should update the @State at your first view.

2      

Apart from xcdatamodel, I have a Student+CoreDataClass...

@objc(Student)
public class Student: NSManagedObject {
}

...a Student+CoreDataProperties...

extension Student {

  @nonobjc public class func fetchRequest() -> NSFetchRequest<Student> {
    return NSFetchRequest<Student>(entityName: "Student")
  }

  @NSManaged public var mail: String?
  @NSManaged public var photo: String?
  @NSManaged public var id: UUID?
  @NSManaged public var image: Data?
  //...

... and a Student+CoreDataMethods.

extension Student {

#if os(iOS)
  func update(moc: NSManagedObjectContext, inputImage: UIImage, name: String, phone: String, mail: String, birthdate: Date, notes: String, instrument: Instrument, course: Course, tutor: Tutor, timestampEdit: Date) {
    let rotatedPhoto = inputImage.fixedOrientation
    let imageData = rotatedPhoto.pngData()
    self.setValue(imageData, forKey: "image")
    //...

I think none of them mark Student as @ObservableObject, doesn't it? If os, you suggest passing it to enviornment on .app file this way?

let studentObject = // How??

var body: some Scene {
        WindowGroup {
          MainView()
            .environment(\.ObservableObject, studentObject)

2      

@Bnerd  

I am not referring to the CoreData entities, myself when I face similar scenarios, I use my separate small "model" file to share information between views. "you suggest passing it to enviornment on .app file this way?" -> Yes. But give it a try first with the Bidings as described in my previous response first, I believe it should work.

2      

I have finally found the way!! Paul himself has had de key there all the time: just an init() and an underscore_ for the FetchRequest for dinamically filtering.

https://www.hackingwithswift.com/books/ios-swiftui/dynamically-filtering-fetchrequest-with-swiftui

Now I build my lists directly from fetchedResults -no intermediate array at all- so deletions display real-time on all displayed views :-)))

Thank your all very much for your suggestions, and Paul for this "treasure".

2      

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 and A/B test your entire paywall UI without any code changes or app updates.

Learn more here

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.