core-dataswiftuicombineobservedobject

Are you allowed to save to the managed object context (Core Data) while typing in a TextEditor (Text Field Input)?


The question is best explained in an example:

struct MyEditor: View {
    @Environment(\.managedObjectContext) var managedObjectContext
    
    @ObservedObject var song: Song
    
    var body: some View {
        TextEditor(text: $song.lyrics)
            .navigationTitle(song.title)
            .onChange(of: song.lyrics) { newValue in
                try? managedObjectContext.save()
            }
    }
}

It feels wrong to spam save but I want to make sure the data is stored. Is this allowed and a correct way to do it?

Another way I can think of is to create a publisher that smoothens the signal to save. If this is the correct way to do it. Can I retrieve the publisher from the ObservedObject or do I have to create a different @State property for that and use onChange to pass the values.

struct MyEditor: View {
    @Environment(\.managedObjectContext) var managedObjectContext
    
    @ObservedObject var song: Song
    
    @State private var lyricsPublisher = PassthroughSubject<String, Never>()
    
    var body: some View {
        TextEditor(text: $song.lyrics)
            .navigationTitle(song.title)
            .onChange(of: song.lyrics) { newValue in
                lyricsPublisher.send(newValue)
            }
            .onReceive(lyricsPublisher
                .debounce(for: 0.5, scheduler: RunLoop.main)
                .removeDuplicates()
            ) { value in
                try? managedObjectContext.save()
            }
    }
}

This is what Song looks like as a ManagedObject.

@objc(Song)
class Song: NSManagedObject, Identifiable {
    @nonobjc class func fetchRequest() -> NSFetchRequest<Song> {
        return NSFetchRequest<Scribble>(entityName: "Song")
    }

    @NSManaged public var id: UUID
    @NSManaged public var title: String
    @NSManaged public var lyrics: String
}

Solution

  • Yes, it is possible to use publisher of managed object (ObservedObject) directly, like

    var body: some View {
        TextEditor(text: $song.lyrics)
            .navigationTitle(song.title)
            .onReceive(song.publisher(for: \.lyrics)      // << here !!
                .debounce(for: 0.5, scheduler: DispatchQueue.main)
                .removeDuplicates()
            ) { value in
                try? managedObjectContext.save()
            }
    }