Reputation: 5
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
}
}
}
Upvotes: 0
Views: 287
Reputation: 15257
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.
Upvotes: 0
Reputation: 15257
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:
Then your app will stop when a core data multi-thread violation happens.
Upvotes: 0