Use The Same View for Adding and Editing Coredata Objects

How to pass a CoreData model item into a view for editing

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:

  • How do I implement a child context (CoreData) in SwiftUI
    environment?
  • SwiftUI - Use @Binding with Core Data
    NSManagedObject?

Editing Core Data List Item Appends Instead of Adding

I have made the following changes in your code.
I have added the oldnamevale and olddescvalue as parameter in the function that can be used for predicate. You have to pass these values.

func modRec(oldnameValue: String, olddescValue: String,newnameValue: String, newdescValue: String, indexPos: NSIndexPath) {

var appDel: AppDelegate = (UIApplication.sharedApplication().delegate as AppDelegate)
var context: NSManagedObjectContext = appDel.managedObjectContext!

var fetchRequest = NSFetchRequest(entityName: "TodayTask")
fetchRequest.predicate = NSPredicate(format: "name = %@", oldnameValue)

if let fetchResults = appDel.managedObjectContext!.executeFetchRequest(fetchRequest, error: nil) as? [NSManagedObject] {
if fetchResults.count != 0{

var managedObject = fetchResults[0]
managedObject.setValue(newdescValue, forKey: "desc")
managedObject.setValue(newnameValue, forKey: "name")

context.save(nil)
}
}

}

If you want execute query using name and desc then make the following changes in the above code

let Predicate1 = NSPredicate(format: "name = %@", oldnameValue)
let Predicate2 = NSPredicate(format: "desc = %@", olddescValue)

var compound = NSCompoundPredicate.andPredicateWithSubpredicates([Predicate1!, Predicate2!])
fetchRequest.predicate = compound

Hope this might be helpful.

Programmatically spawn view after core data item add

Here is possible approach - the idea is to use dynamic binding to state property of newly created object and activate hidden navigation link programmatically.

Tested with Xcode 12.1 / iOS 14.1

@State private var newObject: Object?
private var isNewObject: Binding<Bool> {
Binding(get: { self.newObject != nil }, // activate when state is set
set: { _ in self.newObject = nil }) // reset back
}

...

List {

Button(action: {
addObject()
}, label: { Image(systemName: "plus") })

// List of existing objects, with a button to open ObjectEditor
// and pass in the corresponding object for editing.

}
.background(
NavigationLink(
destination: ObjectEditor(object: newObject),
isActive: isNewObject, // << activated programmatically !!
label: { EmptyView() })
)

...

func addObject() {
withAnimation {
let newObject = Object(context: viewContext)
newObject.title = "New Object"

if let _ = try? viewContext.save() {
self.newObject = newObject // << here !!
}
}
}

SwiftUI: How to show/edit an int from CoreData without being in a List?

....all the tutorials ... show an array containing an Int. Yes, that's because CoreData
can contain many "objects". You get an array of your CountNum objects when
you do your .....var countnum: FetchedResults<CountNum>. So you need to decide which CountNum you want to
use. For example, if you want to use the first one, then:

struct ContentView: View {
@Environment(\.managedObjectContext) var moc
@FetchRequest(sortDescriptors: []) var countnum: FetchedResults<CountNum>

var body: some View {
VStack {
if let firstItem = countnum.first {
Text("+")
.padding()
.onTapGesture(count: 2) {
firstItem.countnum += 1
do {
try moc.save()
} catch {
print(error)
}
}
Text("\(firstItem.countnum)").foregroundColor(.green)
}
}
}
}

EDIT-1: adding new CountNum to CoreData example code in the add button.

struct ContentView: View {
@Environment(\.managedObjectContext) var moc
@FetchRequest(sortDescriptors: []) var countnum: FetchedResults<CountNum>

var body: some View {
Button(action: {add()}) { Text("add new CountNum").foregroundColor(.green) }
.padding(.top, 50)
List {
ForEach(countnum) { item in
HStack {
Text("++")
.onTapGesture(count: 2) { increment(item) }
Text("\(item.countnum)").foregroundColor(.blue)
Text("delete").foregroundColor(.red)
.onTapGesture { delete(item: item) }
}
}
}
}

func increment(_ item: CountNum) {
item.countnum += 1
save()
}

func add() {
let countnum = CountNum(context: moc)
countnum.countnum = 0
save()
}

func delete(item: CountNum) {
moc.delete(item)
save()
}

func save() {
do { try moc.save() } catch { print(error) }
}

}

swiftui how to fetch core data values from Detail to Edit views

Here is a simplified version of your code Just paste this code into your project and call YourAppParent() in a body somewhere in your app as high up as possible since it creates the container.

import SwiftUI
import CoreData
//Class to hold all the Persistence methods
class CoreDataPersistence: ObservableObject{
//Use preview context in canvas/preview
let context = ProcessInfo.processInfo.environment["XCODE_RUNNING_FOR_PREVIEWS"] == "1" ? PersistenceController.preview.container.viewContext : PersistenceController.shared.container.viewContext

///Creates an NSManagedObject of **ANY** type
func create<T: NSManagedObject>() -> T{
T(context: context)
//For adding Defaults see the `extension` all the way at the bottom of this post
}
///Updates an NSManagedObject of any type
func update<T: NSManagedObject>(_ obj: T){
//Make any changes like a last modified variable
//Figure out the type if you want type specific changes
if obj is FileEnt{
//Make type specific changes
let name = (obj as! FileEnt).fileName
print("I'm updating FileEnt \(name ?? "no name")")
}else{
print("I'm Something else")
}

save()
}
///Creates a sample FileEnt
//Look at the preview code for the `FileEdit` `View` to see when to use.
func addSample() -> FileEnt{
let sample: FileEnt = create()
sample.fileName = "Sample"
sample.fileDate = Date.distantFuture
return sample
}
///Deletes an NSManagedObject of any type
func delete(_ obj: NSManagedObject){
context.delete(obj)
save()
}
func resetStore(){
context.rollback()
save()
}
func save(){
do{
try context.save()
}catch{
print(error)
}
}
}
//Entry Point
struct YourAppParent: View{
@StateObject var coreDataPersistence: CoreDataPersistence = .init()
var body: some View{
FileListView()
//@FetchRequest needs it
.environment(\.managedObjectContext, coreDataPersistence.context)
.environmentObject(coreDataPersistence)
}
}
struct FileListView: View {
@EnvironmentObject var persistence: CoreDataPersistence
@FetchRequest(
sortDescriptors: [NSSortDescriptor(keyPath: \FileEnt.fileDate, ascending: true)],
animation: .default)
private var allFiles: FetchedResults<FileEnt>

var body: some View {
NavigationView{
List{
//Has to be lazy or it will create a bunch of objects because the view gets preloaded
LazyVStack{
NavigationLink(destination: FileAdd(), label: {
Text("Add file")
Spacer()
Image(systemName: "plus")
})
}
ForEach(allFiles) { aFile in
NavigationLink(destination: FileDetailView(aFile: aFile)) {
Text(aFile.fileDate?.description ?? "no date")
}.swipeActions(edge: .trailing, allowsFullSwipe: true, content: {
Button("delete", role: .destructive, action: {
persistence.delete(aFile)
})
})
}
}
}
}
}
struct FileListView_Previews: PreviewProvider {
static var previews: some View {
YourAppParent()
// let pers = CoreDataPersistence()
// FileListView()
// @FetchRequest needs it
// .environment(\.managedObjectContext, pers.context)
// .environmentObject(pers)
}
}
struct FileDetailView: View {
@EnvironmentObject var persistence: CoreDataPersistence
@ObservedObject var aFile: FileEnt
@State var showingFileEdit: Bool = false

var body: some View{
Form {
Text(aFile.fileName ?? "")
}
Button(action: {
showingFileEdit.toggle()
}, label: {
Text("Edit")
})
.sheet(isPresented: $showingFileEdit, onDismiss: {
//Discard any changes that were not saved
persistence.resetStore()
}) {
FileEdit(aFile: aFile)
//sheet needs reinject
.environmentObject(persistence)
}
}
}

///A Bridge to FileEdit that creates the object to be edited
struct FileAdd:View{
@EnvironmentObject var persistence: CoreDataPersistence
//This will not show changes to the variables in this View
@State var newFile: FileEnt? = nil
var body: some View{
Group{
if let aFile = newFile{
FileEdit(aFile: aFile)
}else{
//Likely wont ever be visible but there has to be a fallback
ProgressView()
.onAppear(perform: {
newFile = persistence.create()
})
}
}
.navigationBarHidden(true)

}
}
struct FileEdit: View {
@EnvironmentObject var persistence: CoreDataPersistence
@Environment(\.dismiss) var dismiss
//This will observe changes to variables
@ObservedObject var aFile: FileEnt
var viewHasIssues: Bool{
aFile.fileDate == nil || aFile.fileName == nil
}
var body: some View{
Form {
TextField("required", text: $aFile.fileName.bound)
//DatePicker can give the impression that a date != nil
if aFile.fileDate != nil{
DatePicker("filing date", selection: $aFile.fileDate.bound)
}else{
//Likely wont ever be visible but there has to be a fallback
ProgressView()
.onAppear(perform: {
//Set Default
aFile.fileDate = Date()
})
}
}

Button("save", role: .none, action: {
persistence.update(aFile)
dismiss()
}).disabled(viewHasIssues)
Button("cancel", role: .destructive, action: {
persistence.resetStore()
dismiss()
})
}
}

extension Optional where Wrapped == String {
var _bound: String? {
get {
return self
}
set {
self = newValue
}
}
var bound: String {
get {
return _bound ?? ""
}
set {
_bound = newValue
}
}

}
extension Optional where Wrapped == Date {
var _bound: Date? {
get {
return self
}
set {
self = newValue
}
}
public var bound: Date {
get {
return _bound ?? Date.distantPast
}
set {
_bound = newValue
}
}
}

For adding a preview that requires an object you can use this code with the new CoreDataPersistence

///How to create a preview that requires a CoreData object.
struct FileEdit_Previews: PreviewProvider {
static let pers = CoreDataPersistence()
static var previews: some View {
VStack{
FileEdit(aFile: pers.addSample()).environmentObject(pers)
}
}
}

And since the create() is now generic you can use the Entity's extension to add defaults to the variables.

extension FileEnt{ 
public override func awakeFromInsert() {
//Set defaults here
self.fileName = ""
self.fileDate = Date()
}
}


Related Topics



Leave a reply



Submit