I have a minimal sample project at CDPassingQ
My main (ContentView) looks like:
import SwiftUI
import CoreData
struct ContentView: View {
@Environment( \.managedObjectContext ) private var viewContext
@FetchRequest( sortDescriptors: [ NSSortDescriptor( keyPath: \Item.name, ascending: true ) ],
animation: .default )
private var items: FetchedResults<Item>
var body: some View {
NavigationView {
List {
ForEach( items ) { item in
NavigationLink {
NameViewer( itemID: item.objectID )
} label: {
Text( item.name! )
}
}
.onDelete( perform: deleteItems )
}
.toolbar {
ToolbarItem( placement: .navigationBarTrailing ) {
EditButton()
}
ToolbarItem {
Button() {
print( "Add Item" )
} label: {
NavigationLink {
NameViewer();
} label: {
Label( "Add Item", systemImage: "plus" )
}
}
}
}
}
}
private func deleteItems(offsets: IndexSet) {
withAnimation {
offsets.map { items[$0] }.forEach(viewContext.delete)
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)")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
}
}
and NameViewer looks like:
import SwiftUI
import CoreData
enum TrustReason: String, Identifiable, CaseIterable
{
var id: UUID
{
return UUID();
}
case unknown = "Unknown";
case legalOnly = "Legal Only";
case goodLabeling = "Good Labeling";
case facilityClean = "Facility Clean";
case detailedAnswers = "Detailed Answers";
case unresponsive = "Unresponsive";
}
extension TrustReason
{
var title: String
{
switch self
{
case .unknown:
return "Unknown";
case .legalOnly:
return "Legal Only";
case .goodLabeling:
return "Good Labeling";
case .facilityClean:
return "Facility Clean";
case .detailedAnswers:
return "Detailed Answers";
case .unresponsive:
return "Unresponsive";
}
}
}
struct NameViewer: View {
@Environment( \.presentationMode ) var presentationMode
@Environment( \.managedObjectContext ) private var moc
@State private var name: String = ""
@State private var reason: TrustReason = .unknown
var itemID: NSManagedObjectID?
var body: some View {
Form {
Section( header: Text( "Information" ) ) {
TextField( "Name", text: $name )
}
Section( header: Text( "Trust" ) ) {
Picker( "Reason", selection: $reason ) {
ForEach( TrustReason.allCases ) { trustReason in
Text( trustReason.title ).tag( trustReason )
}
}
}
}
.toolbar {
Button() {
if ( saveName() ) {
self.presentationMode.wrappedValue.dismiss()
}
} label: {
Text( "Save" )
}
}
.onAppear {
print( "on appear" )
guard let theID = itemID,
let item = moc.object( with: theID ) as? Item else {
return
}
print( "passed guard" )
if let itemName = item.name {
name = itemName
}
print( name )
}
}
private func saveName() -> Bool {
let item = Item( context: moc )
do {
print( self.name )
item.name = self.name
try moc.save()
return true
} catch {
print( error )
print( error.localizedDescription )
}
self.moc.rollback();
return false
}
}
struct NameViewer_Previews: PreviewProvider {
static var previews: some View {
NameViewer()
}
}
I can create new items to be displayed in the list in ContentView.
Then, when I select an item in the list, I am passing that item to NameViewer. I can confirm that I am successfully finding the correct object in the .onAppear code.
However, there are two problems:
If I select an item in the list, the item name does not appear in the Name TextField unless I click in the text field first.'
Using .onAppear does not seem to be the right place to put that code. The reason is the Picker pushes another view onto the stack and once the item is picked, .onAppear runs again and I lose changes name to the name field.
How can I change the code to resolve these issues?
To implement the desired functionality, I would alter your architecture both on the UI and Core Data sides.
In terms of the user interface, it is best to use navigation links for displaying static data detail views and use modals to carry out data operations, such as creating and editing objects. So have one view to display object detail (e.g. NameViewer
) and another to edit objects (e.g. NameEditor
). Also, bind properties of your NSManagedObject
subclasses directly to SwiftUI controls. Don’t create extra @State
properties and then copy over the values. You’re introducing a shared state, something that SwiftUI is there to eliminate.
On the Core Data side, in order to perform create and update operations, you need to use child contexts. Any time you’re creating or updating your objects show a modal editor view with child context injected. That way if we’re unhappy with our changes, we can simply dismiss that modal and changes are magically discarded without ever needing to call rollback()
, since that child context gets destroyed with the view. Since you’re now using child contexts, don’t forget to save your main view context somewhere too, like when the user navigates out of your app.
So to implement that in code, we need some structs to store our newly created objects as well as child contexts for them:
struct CreateOperation<Object: NSManagedObject>: Identifiable {
let id = UUID()
let childContext: NSManagedObjectContext
let childObject: Object
init(with parentContext: NSManagedObjectContext) {
let childContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
childContext.parent = parentContext
let childObject = Object(context: childContext)
self.childContext = childContext
self.childObject = childObject
}
}
struct UpdateOperation<Object: NSManagedObject>: Identifiable {
let id = UUID()
let childContext: NSManagedObjectContext
let childObject: Object
init?(
withExistingObject object: Object,
in parentContext: NSManagedObjectContext
) {
let childContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
childContext.parent = parentContext
guard let childObject = try? childContext.existingObject(with: object.objectID) as? Object else { return nil }
self.childContext = childContext
self.childObject = childObject
}
}
And the UI code is as follows:
struct ContentView: View {
@Environment(\.managedObjectContext) private var viewContext
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \Item.name, ascending: true)], animation: .default
) private var items: FetchedResults<Item>
@State private var itemCreateOperation: CreateOperation<Item>?
var body: some View {
NavigationView {
List {
ForEach(items) { item in
NavigationLink {
NameViewer(item: item)
} label: {
Text(item.name ?? "")
}
}
}
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
EditButton()
Button(action: {
itemCreateOperation = CreateOperation(with: viewContext)
}) {
Label("Add Item", systemImage: "plus")
}
}
}
.sheet(item: $itemCreateOperation) { createOperation in
NavigationView {
NameEditor(item: createOperation.childObject)
.navigationTitle("New Item")
}
.environment(\.managedObjectContext, createOperation.childContext)
}
}
}
}
struct NameViewer: View {
@Environment(\.managedObjectContext) private var viewContext
@State private var itemUpdateOperation: UpdateOperation<Item>?
@ObservedObject var item: Item
var body: some View {
Form {
Section {
Text(item.name ?? "")
}
}
.navigationTitle("Item")
.toolbar {
Button("Update") {
itemUpdateOperation = UpdateOperation(withExistingObject: item, in: viewContext)
}
}
.sheet(item: $itemUpdateOperation) { updateOperation in
NavigationView {
NameEditor(item: updateOperation.childObject)
.navigationTitle("Update Item")
}
.environment(\.managedObjectContext, updateOperation.childContext)
}
}
}
struct NameEditor: View {
@Environment(\.dismiss) private var dismiss
@Environment(\.managedObjectContext) private var childContext
@ObservedObject var item: Item
var body: some View {
Form {
Section(header: Text("Information")) {
if let name = Binding($item.name) {
TextField("Name", text: name)
}
}
}
.toolbar {
Button() {
try? childContext.save()
dismiss()
} label: {
Text("Save")
}
}
}
}
For more information, see my related answers: