whistler
whistler

Reputation: 550

NSManagedObject changes do not trigger objectWillChange

I have a Core Data model with an entity generated into class Task. I am trying to get the Combine publisher objectWillChange from the NSManagedObject to send (automatically, without manual work), but it won't. The task entity has a name attribute.

let task = Task(context: container.viewContext)

let taskSubscription = task.objectWillChange.sink(receiveValue: { _ in
    print("Task changed")
})

task.name = "Foo"              // WILL NOT trigger

If I call send manually, the subscription will work:

task.objectWillChange.send()   // Will trigger

If I replace this with a simple ObservableObject, it will work as expected:

class DummyTask: ObservableObject {
    @Published var name: String?
}
let dummy = DummyTask()
let dummySubscription = dummy.objectWillChange.sink(receiveValue: { _ in
    print("Dummy changed")
})

dummy.name = "Foo"              // Will trigger
dummy.objectWillChange.send()   // Will trigger

Is NSManagedObject bugged? How should I observe the general entity object for changes? How should I get SwiftUI to see them?

This is using Xcode 11.0 and iOS 13.

Upvotes: 13

Views: 3905

Answers (3)

Nikodem
Nikodem

Reputation: 129

To observe NSManagedObject changes, please look at: https://developer.apple.com/documentation/combine/performing-key-value-observing-with-combine.

Pay attention that, when you use that method in custom class of UICollectionViewListCell or UITableViewCell, you should override the prepareForReuse method and put there alike code:

override func prepareForReuse() {
   super.prepareForReuse()
   nameObserver?.cancel()
   nameObserver = nil
}

Upvotes: -2

Jesse
Jesse

Reputation: 93

My guess is that it is a bug. NSManagedObject conformance to ObservableObject was added in beta 5 which also introduced other significant changes, including deprecation of BindableObject (for replacement by ObservableObject).

See the SwiftUI section: https://developer.apple.com/documentation/ios_ipados_release_notes/ios_13_release_notes)

I ran into the same issue and despite NSManagedObject conforming to ObservableObject, it was not emitting notifications for changes. This might have something to do with NSManagedObject properties needing to be wrapped with @NSManaged which cannot be combined with @Published, while the ObservableObject doc states that, by default, an ObservableObject will synthesize objectWillChange publishers for @Published property changes. https://developer.apple.com/documentation/combine/observableobject

I first tried to get around this by bootstrapping a call to objectWillChange.send() in overrides to Key-Value methods in my NSManagedObject subclass, which only resulted in incorrect behavior.

The solution I went with is the simplest and unfortunately maybe the bulkiest if you need to change a lot of codependent properties in your SwiftUI view. But, so far it is working fine for me and maintains use of SwiftUI as intended.


In Swift:

  1. Create an NSManagedObject subclass for your entity.
  2. In that subclass, create setter methods for the properties you wish to change from your SwiftUI views and at the beginning of the method add a call to objectWillChange.send(), which should look something like this:

    func setTitle(_ text: String) {
        objectWillChange.send()
    
        self.title = text
    }
    

I only advise this as a temporary workaround, as it is not ideal and hopefully will be addressed soon.

I will be submitting a bug report in FeedbackAssistant and I recommend to anyone else encountering this issue to do the same, so we can get Apple to take another look at this!

Edit: A warning about @Anthony’s answer: While the suggested approach does work, be aware that it will not work when changing collection type relationships, i.e. adding an object to an array associated with the NSManagedObject.

Upvotes: 6

Anthony
Anthony

Reputation: 767

I believe it is a bug. There is no point for NSManagedObject to conform to ObservableObject but unable to mark any property as @Published.

While we are waiting Apple to rectify this, I've come across a cleaner solution than @jesseSpencer's suggested one. The logic behind is the same, by adding a objectWillChange.send(), but adding globally into willChangeValue(forKey key: String) instead of adding into individual properties.

override public func willChangeValue(forKey key: String) {  
    super.willChangeValue(forKey: key)  
    self.objectWillChange.send()  
}

Credits: https://forums.developer.apple.com/thread/121897

Upvotes: 14

Related Questions