2

code updated to reflect answer below. Would still love to skip $newName and do something like TextField("New name...", text: $pcard.name) but that throws an error.


long time searcher first time asker. StackOverflow has been invaluable.

Looking for some guidance on best practices to update single "rows" of a core data entity in a Detail View. Clearly I am going about this wrong because I can't get it to work, and there doesn't seem to be a lot of discussion out there about this specific seemingly simple use case.

My app has a standard MasterList/Detail structure, and I would like to bind updates made in the detail view to the entity that was fetched in the List View.

Code below, It's the passing/binding/updating part that I am interested in. Any help appreciated, either conceptually (here's how its designed to work) or practical (do it this way).

Thanks.

import SwiftUI

struct ListView: View {
    @Environment(\.managedObjectContext) private var viewContext
    
    @FetchRequest(
        entity: PCard.entity(),
        sortDescriptors: [NSSortDescriptor(keyPath: \PCard.name, ascending: true)],
        animation: .default
    ) private var pcards: FetchedResults<PCard>
    
    var body: some View {
        NavigationView {
            List {
                ForEach(pcards) { pcard in
                    NavigationLink(destination: DetailView(pcard: pcard)) {
                        Text(pcard.name ?? "no name specified")
                    }
                }
            }
            .navigationTitle("My Cards")
        }
    }
}

struct DetailView: View {
    @Environment(\.managedObjectContext) private var viewContext
    @State private var newName: String = ""
    
    @ObservedObject var pcard: PCard
    
    var body: some View {
        
        VStack {
            Text(pcard.name ?? "no name specified").font(.largeTitle)
            TextField("New name...", text: $newName).textFieldStyle(RoundedBorderTextFieldStyle())
            Button(action: {
                pcard.name = newName
                do {
                    try viewContext.save()
                } catch {
                    let nsError = error as NSError
                    fatalError("Unresolved error \(nsError), \(nsError.userInfo)")
                }
                
            }) {
                //label
                Label("Update", systemImage: "sparkles")
            }
            Spacer()
        }
        .padding()
        
    }
}

struct ListView_Previews: PreviewProvider {
    static var previews: some View {
        ListView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
}

  • core data entities already confirm to ObservableObject protocol internally by default, so instead of passing Binding, you can try using ObservedObject wrapper, and test your calls. --> @ObservedObject var pcard: PCard – Tushar Sharma Mar 01 '21 at 19:31
  • That works! I have updated the code above to reflect. Still wonder if there is a way to do away with the $newItem State/Binding and bind the textfield directly to pcard.name? Not sure if that's possible or what the syntax is. – Leon Rothenberg Mar 01 '21 at 22:31
  • core data properties are optional, here is the solution you need https://stackoverflow.com/questions/57021722/swiftui-optional-textfield. Please don’t update existing post, if you have other problem in code, make a new post. – Tushar Sharma Mar 02 '21 at 07:04

1 Answers1

0

There is no need to create @State newName. @ObservedObject already do what are you looking for. Please read this article: https://purple.telstra.com/blog/swiftui---state-vs--stateobject-vs--observedobject-vs--environme