I am trying to observe individual NSManagedObject changes on NSManagedObjectContextWillSaveNotification:
- (void)managedObjectContextWillSave:(NSNotification *)notification
{
for (NSManagedObject * object in self.mutableObservedManagedObjects)
{
if (object.hasChanges)
{
[self managedObjectWasUpdated:object];
}
}
}
The problem is that hasChanges is true while object.changedValues is empty, thus wrongly (?) triggering managedObjectWasUpdated:.
I'm trying to understand why this is the case and if I should better check object.changedValues.count before calling managedObjectWasUpdated:.
isInserted and isDeleted are both false.
In my experience, if the entity already existed, you loaded it and then you set a value to a property that is equal to its previous value, then the record will be marked as updated, hasChanges will return YES, and changedValues will be empty. When you save the context, what gets updated is a special Core Data column called Z_OPT, which refers to the number of times an entity has been updated. For these situations you can do something like this before saving:
for (NSManagedObject *managedObject in context.updatedObjects.objectEnumerator) {
if (!managedObject.changedValues.count) {
[context refreshObject:managedObject mergeChanges:NO];
}
}
in order to don't even update the Z_OPT value.
I encountered the same issue. Instead of getting the flags, I just checked if changedValues() is empty.
For Swift:
if !managedObject.changedValues().isEmpty {
// Has some changed values
}
From iOS 7 you can also use hasPersistentChangedValues instead of changedValues. I think this performs better.
According to doc, hasChanges will return YES if the receiver has been inserted, has been deleted, or has unsaved changes, otherwise NO.
In your case, you can check isInserted, isUpdated, isDeleted flag to find what happened to your managed object. changedValues only show the properties that have been changed since last fetching or saving the receiver.
Do you have any transient attributes on your entity? I am seeing the behavior you describe, and I've written some test code that shows that modifying a transient attribute causes hasChanges to return true, while changedValues is empty.
You can avoid this behavior by using setPrimitiveValue:forKey: to modify your transient attribute or the equivalent method generated by Core Data (setPrimitiveFoo: for an attribute named foo). You could also implement the transient attribute's setter to do this for you.
When another managed object in relationship to an NSManagedObject has changes, in my observation an NSManagedObject will sometimes, but not always, have an isUpdated of true, while changedValues is empty.
On other occasions, isUpdated is false, and again changedValues is empty.
changedValues being empty seems the correct behavior here. I am unclear what is cause of the variability in isUpdated being true. Having observed this in my production code, I am working on a simple sample project with hope of reproducing this for a bug report to Apple if I can.
To solve my functional problem in my case, I modified my code to automatically set a timestampModified on the parent object in cases where I wanted to ensure that isUpdated was always consistently set to true when these objects in relationship had changed.
Related
I am currently facing a (to me) strange issue as follows:
I have a child MOC (NSManagedObjectContext) with about two dozen MOs (NSManagedObjects). Each MO has an optional Boolean attribute flag. All of these are set to #(NO). fetchedResultsController is an NSFetchedResultsController which fetches these MOs, whose managedObjectContext is moc and whose sectionNameKeyPath is #"flag". fetchedResultsController also has an sort descriptor on flag (and another secondary sort attribute).
After running these lines of code
NSAssert(!moc.hasChanges, nil); // no unsaved changes
BOOL flag = [fetchedResultsController performFetch: &error];
NSAssert(flag && (error == nil), nil); // no errors
I observe the following:
fetchedResultsController.fetchedResults contains as many MOs as does moc and all their flags are #(NO) (as one would expect).
fetchedResultsController.sections.count and fetchedResultsController.sectionIndexTitles.count are both 1 (as one would expect, since all flags have the same value).
fetchedResultsController.sectionIndexTitles[0] is #"1".
The third item appears wrong to me. I would have expected #"0" (since this is the capitalized first letter of [#(NO) description]).
What could be wrong here and how can I obtain the right section index title in this case?
UPDATE I now looks as if the problem may be (still) related to employing flag (an optional Boolean Core Data attribute) as sectionNameKeyPath. Even if values of flag differ, performFetch: leads to only one section titled #"1".
I think it is not reliable to rely on the description implementation of the section title field. This might be appropriate for a string but it seems counter-intuitive that this is the intended solution for numeric values. You probably do not want these values in your final implementation anyway.
You should implement sectionIndexTitleForSectionName, check the boolean value there and return a proper string. Note that this method already receives a string, so you need to check if it is the right one. I suspect it is not, and you have to make sure that the NSNumber generates the correct string. Maybe you need to add a method to NSNumber as a category to return the proper string for the boolean value. Your sectionNameKeyPath would then be something like "flag.toString" or similar.
Another thing you might want to check: depending on how you set up your managed object subclasses and your model, you might have forgotten to de-reference the value of the NSNumber.
BOOL falseValue = #(NO).boolValue; // NO
but
BOOL falseValue = #(NO); // YES
I have a subclass of NSManagedObject on which there's a "currency" attribute. This attribute is a 3 letters string. When I change it from "USD" to "CAD", and then call changedValues on the object, changedValues returns an empty dictionary. Is that the normal behaviour?
I save the managedObjectContext first, then change the attribute, then call changedValues.
This attribute is: not transient, optional, not indexed, no default value.
EDIT: Thx for the help guys I found a bug in my code. Now it works just fine.
I found a bug in my code. Now it works just fine. ;)
I was using a delegate method to update the object from another viewController. When coming back from that viewController I saved the managedObjectContext in viewWillAppear which basically erased the changedValues.
Do it before you save the context.
NSManagedObject Class Reference
changedValues
Returns a dictionary containing the keys and (new) values of
persistent properties that have been changed since last fetching or
saving the receiver.
Can someone please explain what happens to the pointers to the NSManagedObjects after the object is deleted and the context is saved? How should I set them up so that they get set to nil automatically?
Well, it's quite simple.
[managedObjectContext deleteObject:managedObject];
[managedObjectContext save:error];
managedObject = nil;
If you are afraid of memory leaks when deleting lots of objects, just use fast enumeration. This is pretty much guaranteed to clean up behind itself:
for (NSManagedObject *obj in fetchedObjects) {
[managedObjectContext deleteObject:obj];
}
[managedObjectContext save:error];
After you delete an object, the isDeleted property will be true. After saving the context, the isDeleted will be false if you still have a reference to the managed object.
You can safely make weak references to managed objects. The weak reference will nil out automatically for you under ARC when Core Data is done with them.
Here are the three relevant paragraphs from the Core Data Programming Guide:
Core Data “owns” the life-cycle of managed objects. With faulting and
undo, you cannot make the same assumptions about the life-cycle of a
managed object as you would of a standard Cocoa object—managed objects
can be instantiated, destroyed, and resurrected by the framework as it
requires.
When a managed object is created, it is initialized with the default
values given for its entity in the managed object model. In many cases
the default values set in the model may be sufficient. Sometimes,
however, you may wish to perform additional initialization—perhaps
using dynamic values (such as the current date and time) that cannot
be represented in the model.
You should typically not override dealloc to clear transient
properties and other variables. Instead, you should override
didTurnIntoFault. didTurnIntoFault is invoked automatically by Core
Data when an object is turned into a fault and immediately prior to
actual deallocation. You might turn a managed object into a fault
specifically to reduce memory overhead (see “Reducing Memory
Overhead”), so it is important to ensure that you properly perform
clean-up operations in didTurnIntoFault.
I check for (managedObject.managedContext == nil).
If it is nil then it was deleted.
Although this is not guaranteed by Apple Documentation, it seems to be working fine with me.
If you use it in different contexts or it is not saved, it will not work.
See How can I tell whether an `NSManagedObject` has been deleted? for details.
In my NSManagedObject subclass I have an NSString ivar that splits up into an NSSet of entities. I'd like to be able to set the string and during a call to save, do the split, however, only setting the string will not trigger a dirty flag or a need to save.
You can implement the + (BOOL)contextShouldIgnoreUnmodeledPropertyChanges on you NSManagedObject subclass and return NO rather than the default (YES).
This should then cause the NSManagedObjectContext to be notified of changes properties even if they aren't represented by actual columns in the database.
I assume you mean "attribute" instead of "ivar". Your scheme of having a string being split into a set and then saving the set is perhaps debatable, but I guess that is not the issue here.
Why do you need to have the Managed Object marked as "dirty"? This is really not necessary. Just save it, dirty or not!
I do not know how you check the "dirtiness" of your managed object, but I assume you want this to trigger a save at a certain point. At that point you might just as well as check your own BOOL "dirtyFlag" which you can set as appropriate and keep available for checking.
It is always better to make these kinds of things explicit. Your code will become more readable and transparent.
I have an NSManagedObject that has a to-many relationship to another NSManagedObject.
During creation of the NSManagedObject I can use the generated accessors 'removeNotesObject' and the deletion works fine. I can create an object to add to the parent object, save the object, delete the object and then save again. When I fetch this parent object the object I created and deleted is still deleted.
However, after I add the object and then save it (but don't delete and save after) and then fetch it, I can't seem to delete the object that was previously created. I am using the generated accessors to try and remove the object, which appears to work but when I fetch it again the object hasn't been deleted.
(Note: Adding objects does work so it is not a problem with the saving)
To delete the object I retrieve the set of object and select the objects I want to delete. Then I remove the objects
NSSet *notes = summary.notes;
NSSet *oldNotes = [notes objectsPassingTest:^(id obj,BOOL *stop){
Note *oldNote = (Note *)obj;
BOOL sameRow = (oldNote.row == newNote.row);
BOOL sameColumn = (oldNote.column == newNote.column);
BOOL success = (sameRow && sameColumn);
return success;}];
[summary removeNotes:oldNotes];
I have tried making the relationship inverse to delete the objects which didn't delete them. I have also tried different delete rules (cascade and nullify) which again didn't work. Finally, I tried to remove each object separately and deleting each object from the context after I had removed it from the parent object which again unfortunately didn't work.
I assume the problem must be something to do with it being a fetched object. If anyone could help I would really appreciate it as I can't think of any other ways to test or solve this problem.
You need to do
NSManagedObjectContext * moc = .......;
[moc deleteObject:note]
edit: The core data generated accessors simply remove the object from the relationship, but do not delete the object permanently. This makes sense because you may have one NSManagedObject associated to multiple other NSManagedObjects via relationships.
edit: Deleting in the above mentioned fashion will invoke the deletion rules. I suggest you double check that they are setup correctly.
The reason the above code did not work is that == will not actually compare the NSNumber. Instead you need to call 'isEqualTo:'. I think before it was checking the address hence working before I saved it. What's more it was returning an object in the NSSet so appeared to be working. During debugging it wasn't clear what the object was but clearly wasn't the one I needed.