1

I have issue with losing data in Core Data managed objects (all properties in spite of .objectID) are getting to be nil after going lockscreen and back.

I have been debugging this and found such issue.

struct SimpleContactsList: View {

  @FetchRequest var contacts: FetchedResults<Contact>

  let companyId: String

  init(companyId: String) {
    self.companyId = companyId

    self._contacts = FetchRequest(
      entity: Contact.entity(),
      sortDescriptors: [
        NSSortDescriptor(key: "name", ascending: true, selector: #selector(NSString.caseInsensitiveCompare(_:))),
        NSSortDescriptor(keyPath: \Contact.createdAt, ascending: true)
      ],
      predicate: NSPredicate(format: "company.id == %@", companyId)
    )
  }


  var body: some View {

    List {
      ForEach(Array(self.contacts.enumerated()), id: \.1.objectID) { (i, contact) in

        ContactRow(contact: contact)
      }
    }
  }
}

Having above simplifies code snipped it works ok. You can go lock the screen and go back and rows are reloaded with faulted core data managed objects.

But just adding ZStack, VStack, around ContactRow(contact: contact) breaks this and going lockscreen and then back again causes that list reloads with empty managed objects (with nil properties) and list is empty (if I am using optional unwrapping contact.name ?? "") or just crashes the app if I use fore unwrapping o like contact.name!

So this breaks

Changing my SimpleContactLists to this break the code like adding ZStack prevents refreshing list elements from database?

List {
      ForEach(Array(self.contacts.enumerated()), id: \.1.objectID) { (i, contact) in

        ZStack {
          ContactRow(contact: contact)
        }
        .listRowBackground( (i%2 == 0) ? Color("DarkRowBackground") : .white)
        .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
      }
      .onDelete(perform: self.delete)
    }

Empty list loaded from Core Data

UPDATE

contact NSManagedObject is empty but referencing self.contacts[i] IS NOT! Why?

List {

                ForEach(Array(self.contacts.enumerated()), id: \.1.objectID) { (i, contact) in
                    Button(action: {
                        self.selectedId = self.contacts[i].id!
                        self.showDetails = true
                    }) {
                        Contact(contact: contact)
                        //ContactRow(contact: self.contacts[i])
                        .background(Color.white.opacity(0.01))
                    }
                    .buttonStyle(ListButtonStyle())

                    .listRowBackground( (i%2 == 0) ? Color("DarkRowBackground") : .white)
                    .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
                }
                .onDelete(perform: self.delete)
            }

UPDATE 2

I have such solution. It helps in some List-from-FetchResults cases but is not desired solution in more complicated examples like List populated with managed objects in .sheet() (so here I need to stop presenting such sheet)

 @State var theId = UUID()

 List { 
     //rows 
 }
 .id(theId)
 .onReceive(appBecomeActivePublisher, perform: { _ in
        self.theId = UUID()
  }

extension UIApplication { 
   static var didBecomeActivePublisher: AnyPublisher<UIKit.Notification, Never> {

        NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)
            .eraseToAnyPublisher()
    }
}
Michał Ziobro
  • 10,759
  • 11
  • 88
  • 143

1 Answers1

0

Just a guess, but maybe because you're using enumerated() in your ForEach. If what you care about is the index, try using this instead:

ForEach(0..<contacts.count, id: \.self) { i in
    // rest appears to be the same
}

This will actually update now when your fetched results update.

Just to expand on this a bit, but FetchRequest and therefore FetchedResults are update() aware, they are meant to update a swift ui View's body. enumerated() is not, it's just a simple collection. So when that changes, your body doesn't care, it has not subscribed to changes on it. When FetchedResults changes, your body does care, it is subscribed, and therefore it updates itself and rerenders its content.

Procrastin8
  • 4,193
  • 12
  • 25
  • I think it is not related with this as I tested it also without enumareted(). What is a little odd is that self.contacts[i] is set and contact is not. But it is only on this screen on other screens where I've similar FetchRequest > List > Row code there are also empty objects inside self.objects[i]. What I discovered is that I can observe to UIApplication.didBecomeActiveNotification and then update @State var theId. And set this as .id(theId) on List. But it is like forcing entire List refresh (reloadData() on UITableView) but this also loses scroll offset. But I think List should autorefresh – Michał Ziobro Apr 24 '20 at 12:09