Search code examples
formsswiftuicore-databinding

SwiftUI Binding with CoreData in form


I have a Core Data model with multiple properties and a form to edit all of this properties.
I want the changes to be "auto-saved". So as soon as I change something in the form, it is saved to my core data context.

I find myself doing a lot of custom bindings to save the my context on change, for example:

TextField("My string", 
  text: Binding(
    get: {
        model.myString
    }, 
    set: { newValue in
        model.myString = newValue
        try? context.save()
    }
  )
)

I'd like to simplify my code and have a special binding bound to core data context to avoid this boilerplate. How could I simplify this?


Solution

  • If you have

    @Environment(\.managedObjectContext) var context
    @ObservedObject var model: SomeType
    

    At the top of the View

    You can use

     .task(id: model.hasChanges) {
          guard model.hasChanges else {return}
          try? context.save()
      }
    

    It will save anytime there is a change.

    You can put this in a ViewModifier for easy access anywhere where you want to auto save.

    import SwiftUI
    import CoreData
    struct SimpleItemListView: View {
        @FetchRequest( sortDescriptors: []) var items: FetchedResults<SimpleItem>
        var body: some View {
            List(items) { item in
                Text(item.name ?? "")
                    .autoSave(item) // Use like this.
            }
        }
    }
    
    #Preview {
        SimpleItemListView()
            .environment(\.managedObjectContext, PersistenceController.preview.container.viewContext)
    }
    
    //MARK: AutoSave ViewModifier just copy & paste.
    
    extension View {
        func autoSave<MO>(_ object: MO) -> some View where MO: NSManagedObject  {
            modifier(AutoSave(object: object))
        }
    }
    struct AutoSave<MO>: ViewModifier where MO: NSManagedObject {
        @Environment(\.managedObjectContext) var managedObjectContext
        @ObservedObject var object: MO
        @State private var alert: (isPresented: Bool, error: LocalError?) = (false, nil)
        func body(content: Content) -> some View {
            content
                .alert(isPresented: $alert.isPresented, error: alert.error, actions: {
                    Button("Ok") {
                        alert = (false, nil)
                    }
                })
                .task(id: object.hasChanges) {
                    guard object.hasChanges else {return}
                    do {
                        try await managedObjectContext.perform {
                            try managedObjectContext.save()
                        }
                    } catch {
                        alert = (true, LocalError.error(error))
                    }
                }
        }
        private enum LocalError: LocalizedError {
            case error(Error)
            
            var errorDescription: String?{
                switch self {
                case .error(let error):
                    return error.localizedDescription
                }
            }
        }
    }