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
Related
I have a UITableViewController, which is a delegate for an NSFetchedResultsController. My NSFetchedResultsControllerDelegate functions are set up as per "Typical Use" in Apple's documentation, with the table view controller as the fetched result controller's delegate property.
I also have some view controllers which are presented on top of the table view where the managed objects can be modified. These modifications are done on the same managed object context. There is only one managed object context constructed in my application, and it is accessed globally. (I have put some print statements in my object context construction just to be sure I am not accidentally re-constructing it elsewhere.)
When I modify one of the managed objects, the delegate function controller:didChangeObject is called, but the NSFetchedResultsChangeType is always .Delete. When I create a managed object, the delegate function does not fire at all.
However, when I manually call call performFetch() and tableView.reloadData(), the cells are restored to the correct state: the removed row comes back, any not-inserted rows are created.
The result is that deleting an object works as expected (the cell is removed), but updates to an object cause its cell to be removed, and object creations do not trigger cell inserts.
I tried to create a simple demo of this behaviour, but when I re-created the situation from a blank application, I don't see this behaviour. So something within my application is causing this strange behaviour, but I can't figure out what. Any ideas?
Extra Info:
The actual construction of the predicate and sort descriptors are done over several different classes, but printing them via print(resultsController.fetchRequest.predicate) and print(resultsController.fetchRequest.sortDescriptors) gives the following:
Optional(readState == "2" OR readState == "1")
Optional([(readState, ascending, compare:), (title, ascending, compare:)])
I have put a print statement in my controller:didChangeObject: method, and I can see that this only gets called with type.rawValue = 2 (i.e. .Delete), and only when I modify objects, not when I create them.
It's an inconsistency with how NSFetchedResultsController handles its NSPredicate.
If the NSFetchedResultsController is constructed with a fetch request which has a predicate which does a comparison between an integer and a string like follows:
let predicate = NSPredicate(format: "integerAttribute == %#", String(1))
this will lead to the predicate string being:
integerAttribute == "1"
When this is the case, initial fetches work fine: calling the function performFetch() on the fetched results controller returns all objects where the integerAttribute is equal to 1 (where integerAttribute is of type Int32).
However, the notifications to NSFetchedResultsControllerDelegate do not work fine. Modifications of managed objects result in the delegate being notified of a NSFetchedResultsChangeType.Delete change. Creations of managed objects do not invoke the delegate at all.
To make all this weirdness go away, fix the predicate format string as follows:
let predicate = NSPredicate(format: "integerAttribute == %d", 1)
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.
So long story short, there's a discrepancy between the output of a NSMutableDictionary's contents and the result of calling allValues on the same object. Below is some debugger output after inspecting the object which demonstrates my problem: (made generic of course)
(lldb) po self.someDict.allKeys
<__NSArrayI 0xa5a2e00>(
<SomeObject: 0xa5a2dc0>,
<SomeObject: 0xa5a2de0>
)
(lldb) po self.someDict.allValues
<__NSArrayI 0xa895ca0>(
0.5,
0.5
)
(lldb) po self.someDict
{
"<SomeObject: 0xa5a2dc0>" = (null);
"<SomeObject: 0xa5a2de0>" = (null);
}
So as we can see, the actual output of the NSMutableDictionary contains null values for both its entries, but the contents of .allValues contains the proper data. These three outputs were taken at the same time in execution.
I'm not sure why this is happening, but I think it may have something to do with the fact that I'm encoding/decoding the object which this dictionary is a property of using CoreData. I believe I'm doing this properly:
[aCoder encodeObject:self.someDict forKey:#"someDict"];
and to decode
self.someDict = [aDecoder decodeObjectForKey:#"someDict"];
The weird thing is that if I inspect the dictionary before it ever gets encoded, it is still in the state described at the beginning of the post, so this is why I'm doubtful the CoreData actions are screwing with the contents.
As always, please don't hesitate to request additional information.
EDIT: The problem was as answered below. I was using a custom class which didn't cooperate with isEqual, so my solution was to change the storage and structure of my logic, which made using a Dictionary unnecessary.
I have not been able to duplicate the problem using NSString as keys and NSNumber as values. I suspect that your custom class does not properly implement hash and/or isEqual. Specifically, the results from those two methods must be consistent, meaning that if isEqual returns true, then the hash values for the two objects must be identical.
You also need to ensure that your class implements NSCopying properly and that a copy is equal to the original.
As a general rule, don't use custom objects for dictionary keys. Just use strings and be done with it.
As user3386109 points out, custom objects must properly implement the -hash and -isEqual methods in order to be used as dictionary keys, and even then, custom objects don't work correctly for dictionary keys for things like key/value coding.
Problem: Need Unique Identifier
I'm saving a custom object using NSArchiver. It retains all of my objects data, however, everytime I archive and unarchive it gives them new addresses
"<Item: 0x17005d070>",
"<Item: 0x17005e4b0>",
"<Item: 0x17005e4e0>"
"<Item: 0x170059fe0>",
"<Item: 0x170059ec0>",
"<Item: 0x17005a0a0>"
For the same 3 objects.
This causes problems because I need to hold a copy of some of the items and persist this copy, and later compare the copy to the original for equality [currentItem isEqual:oldItem]; Even when this should return true (i.e. the object is the same in terms of name, location, etc.) it will return false because the pointers are different. What's the solution to this? I've thought about adding a uniqueID to each object and then just storing that uniqueID, but that seems like overkill for what I'm trying to do.
Solution:
You can't rely on an objects memory address as a unique identifier. Use NSUUID.
The problem is that you have not implemented isEqual: for your Item class. Implement it. Otherwise, as you've observed, we fall back on NSObject's definition of isEqual:, which is identicality (i.e. these are one and the same object). It is up to you to tell Cocoa that an Item should return true if the object is the same in terms of name, location, etc.; it doesn't magically know this.
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.