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?
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
}
}
}
}