SwiftUI con referencia a un objeto de datos básicos eliminado que causa un bloqueo

8

Me resulta imposible usar datos centrales con SwiftUI, porque cuando paso datos centrales a una variable de objeto de vista observada, la vista del enlace de navegación mantendrá una referencia al objeto incluso después de que la vista haya desaparecido, tan pronto como elimine el objeto del contexto, la aplicación se bloquea, sin mensajes de error.

He confirmado esto envolviendo la variable del objeto de datos centrales en un modelo de vista como opcional, luego configuré el objeto como nulo justo después de la acción de eliminación de contexto y la aplicación funciona bien, pero esto no es una solución porque necesito el objeto de datos central para unirse a las rápidas opiniones de la interfaz de usuario y ser la fuente de la verdad. ¿Cómo se supone que esto funciona? Parece que no puedo hacer nada remotamente complejo con SwiftUI.

He intentado asignar el objeto de datos básicos pasado a un @State opcional, pero esto no funciona. No puedo usar @Binding porque es un objeto recuperado. Y no puedo usar una variable, ya que los controles swiftui requieren enlaces. Solo tiene sentido usar un @ObservedObject, pero esto no puede ser opcional, lo que significa que cuando se elimina el objeto asignado, la aplicación se bloquea porque no puedo configurarlo en cero.

Aquí está el objeto de datos básicos, que es un objeto observable por defecto:

class Entry: NSManagedObject, Identifiable {

    @NSManaged public var date: Date
}

Aquí hay una vista que pasa un objeto de entrada de datos central a otra vista.

struct JournalView: View {

    @Environment(\.managedObjectContext) private var context

    @FetchRequest(
        entity: Entry.entity(),
        sortDescriptors: [],
        predicate: nil,
        animation: .default
    ) var entries: FetchedResults<Entry>

    var body: some View {
        NavigationView {
            List {
                ForEach(entries.indices) { index in
                    NavigationLink(destination: EntryView(entry: self.entries[index])) {
                        Text("Entry")
                    }
                }.onDelete { indexSet in
                    for index in indexSet {
                        self.context.delete(self.entries[index])
                    }
                }
            }
        }
    }
}

Ahora aquí está la vista que accede a todos los atributos del objeto de entrada de datos central que se pasó. Una vez, elimino esta entrada, por cierto, todavía se hace referencia aquí y hace que la aplicación se bloquee de inmediato. Creo que esto también tiene algo que ver con el Enlace de navegación que inicializa toda la vista de destino incluso antes de acceder a ellos. Lo que no tiene sentido por qué haría eso. ¿Es esto un error o hay una mejor manera de lograrlo?

Incluso he intentado hacer la eliminación enDisappear sin éxito. Incluso si hago la eliminación de JournalView, se bloqueará ya que NavigationLink todavía hace referencia al objeto. Es interesante que no se bloquee si elimina un NavigationLink en el que aún no se ha hecho clic.

struct EntryView: View {

    @Environment(\.managedObjectContext) private var context
    @Environment(\.presentationMode) private var presentationMode

    @ObservedObject var entry: Entry

    var body: some View {
        Form {

            DatePicker(selection: $entry.date) {
                Text("Date")
            }

            Button(action: {
                self.context.delete(self.entry)
                self.presentationMode.wrappedValue.dismiss()
            }) {
                Text("Delete")
            }
        }
    }
}

ACTUALIZAR

El bloqueo me está llevando al primer uso de la entrada en EntryView y lee el Tema 1: EXC_BAD_INSTRUCTION (código = EXC_I386_INVOP, subcódigo = 0x0) ... ese es el único mensaje lanzado.

La única solución que se me ocurre es agregar una propiedad al objeto de datos central "isDeleted" y establecerlo en verdadero en lugar de intentar eliminarlo del contexto. Luego, cuando la aplicación se cierra o se inicia, ¿puedo limpiar y eliminar todas las entradas que se han eliminado? No es ideal, y preferiría averiguar qué está mal aquí, ya que parece que no estoy haciendo nada diferente a la muestra MasterDetailApp, que parece funcionar.

SybrSyn
fuente
¡Qué lío! ¿Alguna actualización sobre este @SybrSyn?
Fattie

Respuestas:

4

Básicamente tuve el mismo problema. Parece que SwiftUI carga cada vista inmediatamente, por lo que la vista se ha cargado con las Propiedades del Objeto CoreData existente. Si lo elimina dentro de la Vista donde se accede a algunos datos a través de @ObservedObject, se bloqueará.

Mi solución alternativa:

  1. La acción Eliminar: pospuesta, pero finalizó a través del Centro de notificaciones
    Button(action: {
      //Send Message that the Item  should be deleted
       NotificationCenter.default.post(name: .didSelectDeleteDItem, object: nil)

       //Navigate to a view where the CoreDate Object isn't made available via a property wrapper
        self.presentationMode.wrappedValue.dismiss()
      })
      {Text("Delete Item")}

Debe definir un Notification.name, como:

extension Notification.Name {

    static var didSelectDeleteItem: Notification.Name {
        return Notification.Name("Delete Item")
    }
}
  1. En la vista apropiada, busque el mensaje Eliminar

// Receive Message that the Disease should be deleted
    .onReceive(NotificationCenter.default.publisher(for: .didSelectDeleteDisease)) {_ in

        //1: Dismiss the View (IF It also contains Data from the Item!!)
        self.presentationMode.wrappedValue.dismiss()

        //2: Start deleting Disease - AFTER view has been dismissed
        DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval(1)) {self.dataStorage.deleteDisease(id: self.diseaseDetail.id)}
    }
  1. Esté seguro en sus Vistas donde se accede a algunos elementos de CoreData: ¡compruebe isFault!

    VStack{
         //Important: Only display text if the disease item is available!!!!
           if !diseaseDetail.isFault {
                  Text (self.diseaseDetail.text)
            } else { EmptyView() }
    }

Un poco hacky, pero esto funciona para mí.

sTOOs
fuente
Impresionante, gracias por esta solución. Tendré que intentarlo con esto. Mientras tanto, la solución que se me ocurrió fue agregar un atributo a la entidad llamada "inTrash" y establecerlo en verdadero al eliminar, filtrar la basura en las solicitudes de recuperación y limpiar toda la basura en el lanzamiento, no es ideal, pero esto es trabajando para mí también
SybrSyn
0

Encontré el mismo problema y realmente no encontré una solución al problema raíz. Pero ahora "protejo" la vista que usa los datos referenciados como este:

var body: some View {
    if (clip.isFault) {
        return AnyView(EmptyView())
    } else {
        return AnyView(actualClipView)
    }
}

var actualClipView: some View {
    // …the actual view code accessing various fields in clip
}

Eso también se siente hacky, pero funciona bien por ahora. ¡Es menos complejo que usar una notificación para "diferir" la eliminación, pero aún así, gracias a la respuesta de sTOO por la pista .isFault!

Benjamin Graf
fuente