Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI - Use @Binding with Core Data NSManagedObject?

I'm trying to make an edit form that can take a value as a @Binding, edit it, and commit the change. In this case, I'm populating a list with Core Data records using the @FetchRequest property wrapper. I want to tap on a row to navigate from the List to a Detail view, then on the Detail view I want to navigate to the Edit view.

Example image

I tried doing this without the @Binding and the code will compile but when I make an edit, it is not reflected on the previous screens. It seems like I need to use @Binding but I can't figure out a way to get a NSManagedObject instance inside of a List or ForEach, and pass it to a view that can use it as a @Binding.

List View

struct TimelineListView: View {

    @Environment(\.managedObjectContext) var managedObjectContext

    // The Timeline class has an `allTimelinesFetchRequest` function that can be used here
    @FetchRequest(fetchRequest: Timeline.allTimelinesFetchRequest()) var timelines: FetchedResults<Timeline>

    @State var openAddModalSheet = false

    var body: some View {

        return NavigationView {
            VStack {
                List {

                    Section(header:
                        Text("Lists")
                    ) {
                        ForEach(self.timelines) { timeline in

                            // ✳️ How to I use the timeline in this list as a @Binding?

                            NavigationLink(destination: TimelineDetailView(timeline: $timeline)) {
                                TimelineCell(timeline: timeline)
                            }
                        }
                    }
                    .font(.headline)

                }
                .listStyle(GroupedListStyle())

            }

            .navigationBarTitle(Text("Lists"), displayMode: .inline)

        }

    } // End Body
}

Detail View

struct TimelineDetailView: View {

    @Environment(\.managedObjectContext) var managedObjectContext

    @Binding var timeline: Timeline

    var body: some View {

        List {

            Section {

                NavigationLink(destination: TimelineEditView(timeline: $timeline)) {
                    TimelineCell(timeline: timeline)
                }

            }

            Section {

                Text("Event data here")
                Text("Another event here")
                Text("A third event here")

            }


        }.listStyle(GroupedListStyle())

    }
}

Edit View

struct TimelineEditView: View {

    @Environment(\.managedObjectContext) var managedObjectContext

    @Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>

    @State private var newDataValue = ""

    @Binding var timeline: Timeline

    var body: some View {

        return VStack {

            TextField("Data to edit", text: self.$newDataValue)
                .shadow(color: .secondary, radius: 1, x: 0, y: 0)
                .textFieldStyle(RoundedBorderTextFieldStyle())
                .onAppear {
                    self.newDataValue = self.timeline.name ?? ""
            }.padding()
            Spacer()
        }

            .navigationBarItems(
                leading:
                Button(action: ({
                    // Dismiss the modal sheet
                    self.newDataValue = ""
                    self.presentationMode.wrappedValue.dismiss()

                })) {
                    Text("Cancel")
                },

                trailing: Button(action: ({

                    self.timeline.name = self.newDataValue


                    do {
                        try self.managedObjectContext.save()
                    } catch {
                        print(error)
                    }

                    // Dismiss the modal sheet
                    self.newDataValue = ""
                    self.presentationMode.wrappedValue.dismiss()

                })) {
                    Text("Done")
                }
        )

    }
}

I should mention, the only reason I'm even trying to do this is because the modal .sheet() stuff is super buggy.

like image 862
radicalappdev Avatar asked Aug 22 '19 17:08

radicalappdev


2 Answers

@Binding only works with structs.

But CoreData result are Objects (NSManagedObject adopting ObservableObject). You need to use @ObservedObject to register for changes.

like image 167
nine stones Avatar answered Nov 05 '22 10:11

nine stones


To implement creation and editing functionality with Core Data it is best to use nested managed object contexts. If we inject a child managed object context, derived from the main view context, as well as the managed object being created or edited that is associated with a child context, we get a safe space where we can make changes and discard them if needed without altering the context that drives our UI.

    let childContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)
    childContext.parent = viewContext
    let childItem = childContext.object(with: objectID) as! Item
    return ItemHost(item: childItem)
        .environment(\.managedObjectContext, childContext)

Once we are done with our changes, we just need to save the child context and the changes will be pushed up to the main view context and can be saved right away or later, depending on your architecture. If we are unhappy with our changes we can discard them by calling rollback() on our child context.

    childContext.rollback()

Regarding the question of binding managed objects with SwiftUI views, once we have our child object injected into our edit form, we can bind its properties directly to SwiftUI controls. This is possible since NSManagedObject class conforms to ObservableObject protocol. All we have to do is mark a property that holds a reference to our child object with @ObservedObject and we get its bindings. The only complication here is that there are often type mismatches. For example, managed objects store strings as String?, but TextField expects String. To go around that we can use Binding’s initializer init?(_ base: Binding<Value?>).

We can now use our bindings, provided that the name attribute has a default empty string set in the managed object model, or else this will crash.

    TextField("Title", text: Binding($item.title)!)

This way we can cleanly implement the SwiftUI philosophy of no shared state. I have provided a sample project for further reference.

like image 8
Gene Bogdanovich Avatar answered Nov 05 '22 10:11

Gene Bogdanovich