1

I'm learning Core Data but a strange problem confused me at the very beginning. I was using the Empty project created by Xcode and did some modifications to try to implement the feature to add a new entity in a sheet.

I wanted to track the item to be added by a @State value. However, when I open up the sheet, I have already seen the record is added before I executed any try? context.save(). I'd like to use the @State entity to be passed down to the sheet for receiving information there and finally saved to store when hitting "confirm" (not implemented in the code). The reason I pass a whole entity object is that I want to handle Add/Edit in the same sheet.

BTW, is it correct to handle such "Entity adding" scenario like this using @State?

import SwiftUI import CoreData struct ContentView: View { @Environment(\.managedObjectContext) private var viewContext @FetchRequest( sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)], animation: .default) private var items: FetchedResults<Item> @State private var itemToAdd: Item? var body: some View { NavigationView { List { ForEach(items) { item in NavigationLink { Text("Item at \(item.timestamp!, formatter: itemFormatter)") } label: { Text(item.timestamp!, formatter: itemFormatter) } } } .toolbar { ToolbarItem { Button { itemToAdd = Item(context: viewContext) itemToAdd?.timestamp = Date() } label: { Label("Add Item", systemImage: "plus") } } } .sheet(item: $itemToAdd) { itemToAdd in Text("Empty") } } } } private let itemFormatter: DateFormatter = { let formatter = DateFormatter() formatter.dateStyle = .short formatter.timeStyle = .medium return formatter }() 
2
  • "Correct" is opinion based, which is out of scope here. It is a way to do this and yes it gets added. But until you save it is in a sort of cache. it will disappear if you restart the app. With NSFetchRequest you can exclude these items and the re context can be reset/rollback to get rid of any pending changes. Here is a similar setup. Commented Mar 10, 2022 at 16:29
  • In Core Data, see what @loremipsum wrote. If you want to evaluate what comes back from the sheet before creating the database item, you can create a temporary Item struct that is used only between the view and the sheet. When you have validated the temporary object, you can create the Core Data Item. Commented Mar 10, 2022 at 16:36

2 Answers 2

2

Usually we create a child context to use a "scratch pad" for creating objects. So if cancelled, the context is thrown away without affecting the main context. You can achieve this with a struct that creates a child context and an the new object, and use that struct as your sheet item. E.g.

struct ItemEditorConfig: Identifiable { let id = UUID() let context: NSManagedObjectContext let item: Item init(viewContext: NSManagedObjectContext, objectID: NSManagedObjectID) { // create the scratch pad context context = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType) context.parent = viewContext // load the item into the scratch pad item = context.object(with: objectID) as! Item } } struct ItemEditor: View { @ObservedObject var item: Item // this is the scratch pad item @Environment(\.managedObjectContext) private var context @Environment(\.dismiss) private var dismiss // causes body to run let onSave: () -> Void @State var errorMessage: String? var body: some View { NavigationView { Form { Text(item.timestamp!, formatter: itemFormatter) if let errorMessage = errorMessage { Text(errorMessage) } Button("Update Time") { item.timestamp = Date() } } .toolbar { ToolbarItem(placement: .navigationBarLeading) { Button("Cancel") { dismiss() } } ToolbarItem(placement: .navigationBarTrailing) { Button("Save") { // first save the scratch pad context then call the handler which will save the view context. do { try context.save() errorMessage = nil onSave() } catch { let nsError = error as NSError errorMessage = "Unresolved error \(nsError), \(nsError.userInfo)" } } } } } } } struct EditItemButton: View { let itemObjectID: NSManagedObjectID @Environment(\.managedObjectContext) private var viewContext @State var itemEditorConfig: ItemEditorConfig? var body: some View { Button(action: edit) { Text("Edit") } .sheet(item: $itemEditorConfig, onDismiss: didDismiss) { config in ItemEditor(item: config.item) { do { try viewContext.save() } catch { // Replace this implementation with code to handle the error appropriately. // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. let nsError = error as NSError fatalError("Unresolved error \(nsError), \(nsError.userInfo)") } itemEditorConfig = nil // dismiss the sheet } .environment(\.managedObjectContext, config.context) } } func edit() { itemEditorConfig = ItemEditorConfig(viewContext: viewContext, objectID: itemObjectID) } func didDismiss() { // Handle the dismissing action. } } struct DetailView: View { @ObservedObject var item: Item var body: some View { Text("Item at \(item.timestamp!, formatter: itemFormatter)") .toolbar { ToolbarItem(placement: .navigationBarTrailing) { EditItemButton(itemObjectID: item.objectID) } } } } 
Sign up to request clarification or add additional context in comments.

2 Comments

Thanks for letting me know the idea of "child context". If I embed one more sheet in ItemEditor (e.g. editing more settings there) with the similar "cancel-save" pattern, I will have to create another "child context" and save-callback, right?
Yes you can do it that way. If you are new to child contexts you might want to read the docs. Basically when you save the child context it pushes the changes up to the parent context. You have to learn how to merge changes from parent back down to children if you want that. You need to also learn the different merge behaviors. Here is the doc, see from "When you save changes in a context," developer.apple.com/documentation/coredata/…
-1

You added the item to the ViewContext, but you have not persisted it to storage. If you click you "+" button, it appears in the list, but if you stop the app and reopen it, you will find the item is not there.

Think of the view context as a world of data that is created from the persistent store. You can put things in and change things, but they are not changed in the store until you call save(). In this way, you have done nothing different than if you used a data model in a view model with an @Published var like this:

class ViewModel: ObservableObject { @Published items: [Items] init() { ... } } struct item: Identifiable { let id = UUID() var timeStamp: Date } 

If you implemented this instead of Core Data, you would see the same behavior, but for the save()

4 Comments

"view models have no place in SwiftUI" stackoverflow.com/a/60883764/259521
I think you missed the point of the answer AND are injecting opinion. This was a demonstration as to how Core Data works if you don't use a .save(). As Core Data creates a class to manage the objects (whether you see it in your code or not), this is a valid paradigm for someone JUST LEARNING Core Data to be able to relate to. The OP asked why did this happen.
your code isn't core data though
I agree. The question was why did the managed object was added to the context before it was saved. My answer was an example of what was happening in a non-Core Data way. The OP is just learning Core Data, and obviously did not understand that creating a managed object adds it to the context, even though the context had not been persisted. The above was the closest non-CD example I could think of to show what was happening. Secondarily he asked about keeping in an @State var for editing. OP's code works, though it is naive. This was about understanding why it did what it did.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.