Data flow and managed objects in SwiftUI

Hello, I'm still struggling with passing data around in SwiftUI with Core Data.

In my test app currently theres a button on top to add a group of six grocery entries. I'm trying to show the list of "entries" in the view and when tapped a detail .sheet modal is presented with the tapped/chosen item passed in. Then when the entry detail is tapped the item is deleted from storage.

I'm having two distinct issues:
  • the detail view only ever shows the first item in the entries. In this case, "Butter Detail" is always presented.

  • when tapped, the detail text crashes the app with the error "An NSManagedObjectContext cannot delete objects in other contexts". I though @StateObject could help me here but I'm still unclear on its implementation.

Currently the .sheet model is within the ForEach, because it cant find the "entry in". Should I be storing the "entry in" in a @Stat var and passing that into the modal? I don't know how to store an entity in that manner.


Code Block
struct ContentView: View {
@Environment(\.managedObjectContext) var managedObjectContext
@FetchRequest(entity: Entry.entity(), sortDescriptors: [NSSortDescriptor(keyPath: \Entry.name, ascending: true)], predicate: nil) var entries: FetchedResults<Entry>
@State var isPresented = false
var body: some View {
        VStack {
            Button(action: {
                Entry.create(in: managedObjectContext)
            }) {
                Text("Button")
            }
            Text("\(entries.count)")
            List{
                ForEach(entries, id: \.self) { entry in
                    Text(entry.name ?? "Unknown")
                        .onTapGesture {
                            isPresented = true
                        }
                        .sheet(isPresented: $isPresented) {
                            Detail(entry: entry)
                   }
                }
            }
        }
    }
}
struct Detail: View {
@Environment(\.managedObjectContext) var managedObjectContext
@StateObject var entry: Entry
    var body: some View {
        Text("\(entry.name ?? "Error") Detail")
            .onTapGesture {
                do {
                    managedObjectContext.delete(entry)
                    try managedObjectContext.save()
                } catch {
                    print(error)
                }
            }
    }
}


Answered by DelawareMathGuy in 618439022
hi Oghweb,

after i posted my response, i did a quick edit after thinking about @State being used with a class -- but by the time i looked this up in my own code, it was too late to re-edit (posts close to editing after about ten minutes). sorry for the confusion.

i have two things for you:
  • i was right the first time in my code: @State should be used, not @ObservedObject.

  • be sure you have @State private var selectedEntry: Entry? SceneDelegate would only complain if you left off the private access control.

FWIW: i have a bunch of code i'm developing out in public that deals with a lot of what you're looking at. it's basically my SwiftUI playground. please feel free to take it and use what you can (or ask a question later about it); you'll find a lot of similarity. it's called ShoppingList and you can find it on Github (be sure you find my project; don't be confused the two thousand other things on Github named with some combination or variation of the words "shopping" and "list").

hope that helps,
DMG

hi,

i think these ideas might help.
  • you don't want a .sheet modifier on each list item; but you do want a .sheet modifier (just one) probably attached to the List.

  • but to tie that .sheet to whichever list item is tapped, you need to off-load the tapped entry to a local variable to make the connection to the .sheet

  • a sheet does not inherit the managed object context of its parent. you need to add that explicitly with an .environment modifier..

so here's some proposed code for you (i haven't tested, but i have used this technique before). first add a variable to your View (it should be @State so you can write to it -- but on second thought, you may need to make this an @ObservedObject)

Code Block
@State private var selectedEntry: Entry? // or @ObservedObject, since Entry is a class and not a struct ??

then rewrite your interior List code to be

Code Block
List {
ForEach(entries, id: \.self) { entry in
Text(entry.name ?? "Unknown")
.onTapGesture {
isPresented = true
selectedEntry = entry
}
}
}
.sheet(isPresented: $isPresented) {
Detail(entry: selectedEntry!).environment(\.managedObjectContext, managedObjectContext)
}

you may need some self. qualifiers in the code above, if you are using Xcode 11; and your detail view can retrieve its managedObjectContext using @Environment(\.managedObjectContext).

hope that helps,
DMG

Thanks DelawareMathGuy, this is really helpful. Specifying the environment solved the issue of managing object from the detail view.

As far as passing the right entry to the detail view I tried both methods without success. The off-loading of the entry to a local variable is how I've done it in the past but Ive never done it with Core Data. Right now when I do @ObservedObject it says ".self is immutable" in the view body. When I put @State SceneDelegate begs for a "chosenEntry" in the call to ContentView. I cant forge out how to declare an Entry type without it bugging out.

I appreciate the tips
Accepted Answer
hi Oghweb,

after i posted my response, i did a quick edit after thinking about @State being used with a class -- but by the time i looked this up in my own code, it was too late to re-edit (posts close to editing after about ten minutes). sorry for the confusion.

i have two things for you:
  • i was right the first time in my code: @State should be used, not @ObservedObject.

  • be sure you have @State private var selectedEntry: Entry? SceneDelegate would only complain if you left off the private access control.

FWIW: i have a bunch of code i'm developing out in public that deals with a lot of what you're looking at. it's basically my SwiftUI playground. please feel free to take it and use what you can (or ask a question later about it); you'll find a lot of similarity. it's called ShoppingList and you can find it on Github (be sure you find my project; don't be confused the two thousand other things on Github named with some combination or variation of the words "shopping" and "list").

hope that helps,
DMG

Okay I've cracked it open and tried out the method of passing in the data rather than the object itself by offloading to a struct. That definitely solves the issue, optically, but I need to refactor editing of deleting the item from the modal. I'm sure i'll figure it out tonight. If I get stuck again or have further questions I'll be back! Regardless, thanks DMG for the help and great ShoppingList project as a resource!
Data flow and managed objects in SwiftUI
 
 
Q