SwiftedMind
SwiftedMind

Reputation: 4287

NSManagedObjectContextObjectsDidChange notification only sent when the modified managed object has been accessed before. Why?

(See Updates below for more detailed info.)

this is a strange problem. I can't post everything in my class (it's way too big), so I'll try to cover the essential parts:

I have a view controller with a viewDidLoad() method:

class MyClass {
    ...
    override func viewDidLoad() {
        super.viewDidLoad()        

        // mainViewContext is the viewContext passed by AppDelegate's persistentContainer
        NotificationCenter.default.addObserver(self, selector: #selector(test(notif:)), name: Notification.Name.NSManagedObjectContextObjectsDidChange, object: mainViewContext)
    }
    ...
    @objc func test(notif: Notification) {
        print("I got called")
    }
    ...

So what I did is adding an observer so that I know if a CoreData object has been changed. However, this notification doesn't get posted always. When I do this (for example in viewDidLoad() right after adding the observer):

self.appDelegate.persistentContainer.performBackgroundTask({ (privateContext) in
    // FileBrowserElement is a managed object I created
    let folder = privateContext.object(with: App.rootFolderObjectID) as! FileBrowserElement
    // Random number so that the name really changes everytime (so that there is something to change when saving)
    folder.name = "newFolderName" + String(Int.random(between: 0, and: 90000))

    do {
        try privateContext.save()
    } catch let error as NSError {
        print("Error: " + error.debugDescription)
    }
})

it is working. test(notif:) is being called. But now I replace it with this:

self.appDelegate.persistentContainer.performBackgroundTask({ 
(privateContext) in
    // Just another type of managed object I have.
    let folder = privateContext.object(with: App.templateRackObjectID) as! Rack
    folder.name = "newFolderName" + String(Int.random(between: 0, and: 90000))

    do {
        try privateContext.save()
    } catch let error as NSError {
        print("Error: " + error.debugDescription)
    }
})

And it's not working anymore. test(notif:) isn't being called. I have no idea why that is. I literally only changed the object that's being modified.

(I did set persistentContainer.viewContext.automaticallyMergesChangesFromParent = true in AppDelegate)

Is the notification only posted on some kinds of managed objects? Is there something I miss? I don't know how to start debugging this. I've been trying for hours. It is extremely strange.

any thoughts? Let me know if you need any more info. As I said, I don't know what's important for this issue. The whole class would be too big to post here.

UPDATE: I have tried to recreate this problem with a dummy project. Strangely, it's not posting a notification with either managed object. I have uploaded it to github: https://github.com/d3mueller/NotificationTestProject

Maybe I've done something wrong there, that I don't see?

UPDATE 2 (found something. I updated the repo) So, this is incredibly strange. I had a suspicion that turned out to be true (I guess).

It seems, that the notification is only posted when you previously loaded the same managed object that you want to modify and access an attribute of it. I added this right before starting the performBackgroundTask:

// As a class attribute, I added `private var rack: Rack!`. Then this is in `viewDidLoad`, right before the background task
   rack = mainViewContext.object(with: App.templateRackObjectID) as! Rack
            _ = rack.name

Now it's working. But why? This is strange. Why do I have to access it before? The notification should always be sent. Any thoughts on this? I added this to the dummy project on Github if you want to try it out.

I'd really appreciate any help with this :)

Upvotes: 3

Views: 1315

Answers (2)

Antonia Zhang
Antonia Zhang

Reputation: 103

Have you tried setting the object to your addObserver to nil?

It seems to me you are observing the notification from a different context than the one you made changes to. In this Documentation, it is said “Each time this method is invoked, the persistent container creates a new NSManagedObjectContext.” So when you performBackgroundTask, you are making changes at a brand new context, and you are only listening changes to mainContext, you shouldn’t be receiving any notifications at all? I’m surprised it worked the first time.

And when automaticallyMergesChangesFromParent, It is said here: documentation “indicates whether the context automatically merges changes saved to its persistent store coordinator or parent context.” I didn’t see anywhere that the new context created in performBackgroundTask call would be a child context to any other context, and presumably not to mainContext here either. When the privateContext is saved though, the coordinator should receive some changes, but the coordinator isn’t sending out the notification either.

mergeChanges(fromContextDidSave:) is probably needed instead, if you want to merge the background context into the main context and only receive notifications from there.

Upvotes: 2

Maurice
Maurice

Reputation: 1464

Stumbled upon this today too. The context does not retain the objects and therefore does not „update“ your objects. I found that setting retainsRegisteredObjects to true will cause the context to start sending the expected notifications.

Upvotes: 0

Related Questions