0

I've been trying to solve this problem for some time, to no avail.
When accessing a managed object from UserNotification custom action and then trying to save the changes to this object I get the following message:

[error] error: CoreData: error: Failed to call designated initializer on NSManagedObject class 'NSManagedObject'.

Basically, the setup is as follows:
1. User gets a notification
2. Chooses a custom action
3. From the info in the notification the UserNotification Center Delegate extracts the URI of the object and then extracts it from the persistent store
4. Once done and type-casted the delegate calls appropriate method on the object
5. After method returns delegate tries to save the context, and that's where the error appears.

Here is some relevant code:

// - UNUserNotification Centre delegate
extension HPBUserNotificationsHandler: UNUserNotificationCenterDelegate {    
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {

    guard let object = getAssociatedObject(id: response.notification.request.identifier) else { return }

    switch response.actionIdentifier {
    case UNNotificationDismissActionIdentifier:
        ....
    case UNNotificationDefaultActionIdentifier:
        ....
    case HPBReminderAction.take.rawValue:
        // get intake object
        guard let reminder = object as? HPBIntake else { return }
        reminder.take(at: Date()) 
        try! dataController.saveContext() // here is when the error is raised
    default:
        break
    }

    completionHandler()
}

The function to extract an object from persistent store:

func getAssociatedObject(id: String) -> NSManagedObject? {
    guard let psc = dataController.managedObjectContext.persistentStoreCoordinator else { return nil }
    guard let objURL = URL(string: id) else { return nil }
    guard let moid = psc.managedObjectID(forURIRepresentation: objURL) else { return nil }
    return dataController.managedObjectContext.object(with: moid)
}

If I make the same changes on this object directly in the app - everything works. So I assume the matter is with getting the object from a custom action on User Notification. But I can't figure out what is the problem.

Here is some additional info. When I inspect the reminder object right before calling the take(on:) function, it shows as a fault:

Home_Pillbox.HPBIntake: 0x7fb1a9074e90 (entity: Intake; id: 0x7fb1a9069e50 x-coredata:///Intake/tAC4BBCD4-B128-4C6F-8E1B-2EE7D4EDBCB34 ; data: fault)

Of course, when the function is called, the fault is fired but the object is not initialised correctly and instead populates all properties as nil:

Home_Pillbox.HPBIntake: 0x7fb1a9074e90 (entity: Intake; id: 0x7fb1a9069e50 x-coredata:///Intake/tAC4BBCD4-B128-4C6F-8E1B-2EE7D4EDBCB34 ; data: {dosage = 0; identifier = nil; localNotification = nil; log = nil; meal = 0; medName = nil; notificationRequest = nil; profileName = nil; schedule = nil; status = 1; treatment = nil; unit = 0; userNotes = nil;})

So when the context tries to save it can't, as properties are nil, which is not allowed by the data model. What also bothers me is that the error mentions designated initialiser on NSManagedObject instead of the name of the subclass HPBIntake, even though the object is clearly correctly typed.

Any help will be highly appreciated.

EDIT: Here's the implementation of saveContext() function in the DataController:

    func saveContext() throws {
        if managedObjectContext.hasChanges {
            do {
                try managedObjectContext.save()
            } catch let syserr as NSError {
                throw syserror
            }
        }
    }
seiji594
  • 5
  • 5
  • Hi, welcome to SO! Many will try to help you, but your question is not completely clear to me. What does e.g. `dataController.saveContext()`? – Reinhard Männer Feb 22 '19 at 20:04
  • This looks like another instance of [this problem](https://stackoverflow.com/a/33307824/3985749) - check your code for calls to the bare initialiser. But failing that I suspect it’s a concurrency problem - are you using a background context and/or dispatching to the main thread? – pbasdf Feb 22 '19 at 21:22
  • @ReinhardMänner, `dataController.saveContext()` saves the managed object context. The `dataController` is a singleton of `DataController` class, which initialises the core data stack and handles queries and the changes to the context. Mostly it's boilerplate stuff, taken from Apple docs. @pbasdf, I only have one context in the app; my understanding is that everything happens on the main thread when user interactions are concerned. But maybe I do need to investigate the concurrency further. – seiji594 Feb 24 '19 at 15:29
  • The [Apple example](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CoreData/InitializingtheCoreDataStack.html) of a `DataController` just initialises the core data stack. It does not have a function `saveContext()`. So maybe could can show your implementation? – Reinhard Männer Feb 25 '19 at 17:21
  • @ReinhardMänner I posted the code under [EDIT]. Nothing really interesting there. – seiji594 Feb 26 '19 at 17:23

2 Answers2

0

Just an idea: You said

If I make the same changes on this object directly in the app - everything works.

I assume you do these changes on the main thread.

Did you check if userNotificationCenter(_:didReceive:withCompletionHandler:) is also executed on the main thread? If not, you might have a problem here, since core data expects to be executed on one thread only. In this case you could try to execute the body of this function with

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
  DispatchQueue.main.async {
    // Your code here
  }
}

You should also set the launch argument -com.apple.CoreData.ConcurrencyDebug 1 in your scheme, together with exception breakpoints:
enter image description here Then your app will stop when a core data multi-thread violation happens.

Community
  • 1
  • 1
Reinhard Männer
  • 14,022
  • 5
  • 54
  • 116
  • I checked it - the code does run on the main thread. To double-check I followed your advice by setting the launch argument - nothing was caught; I even executed the code inside main.async{ } block just in case - same problem. So the issue must be somewhere else. I guess I'll just have to keep digging. – seiji594 Feb 25 '19 at 16:51
  • Too bad - would have been too easy... Good luck! – Reinhard Männer Feb 25 '19 at 16:54
0

One more idea: Since you get an initializer error, it seems to me that the object whose id you transfer via the notification is not yet initialized when you try to save it.
In this link I found the following:

object(with:) throws an exception if no record can be found for the object identifier it receives. For example, if the application deleted the record corresponding with the object identifier, Core Data is unable to hand your application the corresponding record. The result is an exception.
The existingObject(with:) method behaves in a similar fashion. The main difference is that the method throws an error if it cannot fetch the managed object corresponding to the object identifier.

So I suggest to replace in getAssociatedObject(id: String) the call to object(with: moid) by existingObject(with: moid). If this throws an error, you know that the related object does not yet or no longer exist.
If this is allowed by your app, you had to initialize it by the designated initialiser

init(entity entity: NSEntityDescription, insertIntoManagedObjectContext context: NSManagedObjectContext?)  

before you try to store it.

EDIT:

In this answer, you can find more suggestions how to debug your core data handling.

Reinhard Männer
  • 14,022
  • 5
  • 54
  • 116
  • Yes, it does indeed throw an error when I replace `object(with:)` by `existingObject(with:)`. This is a mystery to me as the object does absolutely exist - it can be verified in various ways. Even so, thanks a lot for your suggestion! It doesn't solve my problem but it does point to the direction of further investigation. – seiji594 Feb 28 '19 at 11:11
  • Glad that I could help. When you find the problem and it is related to your question, please consider to add and accept your own answer, which might be helpful for others. If it is unrelated, consider to accept my answer so that the question is closed. – Reinhard Männer Mar 01 '19 at 06:13
  • Ok, I finally got to the bottom of this. The URI of the object was being inserted into the notification content at the time of object creation. At this point the object ID (and its URL) is temporary and is replaced by permanent ID as soon as the context is saved. But the notification object is never updated to reflect this, of course. Quite an embarrassing mistake. I had to refactor quite a bit of my code, but now it all works. So I am marking your answer @ReinhardMänner as the accepted one. Again, thanks a lot for the pointer. – seiji594 Mar 08 '19 at 17:01
  • Thanks for the comment! Interesting! I would have never thought about this. – Reinhard Männer Mar 08 '19 at 18:27