1

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.

Community
  • 1
  • 1
Andrew Bennet
  • 2,600
  • 1
  • 21
  • 55
  • If the cell is removed when you update I'd expect you to get a crash if that didn't match the actual context contents... I guess you should delete the SQL file and try again. Then delete and recreate the model. I don't think there is a sensible explanation for this... – Wain Jun 01 '16 at 22:00
  • 1
    Do you specify a predicate for the FRC? If so, do the updates result in the objects being excluded by the predicate, and hence removed from the TV? – pbasdf Jun 01 '16 at 22:07
  • @pbasdf good idea, but not the solution in this case. I do specify a predicate, but it's nothing which would prevent the object from being included. When I refetch and call reloadData on the table (still with the predicate), the cell comes back – Andrew Bennet Jun 01 '16 at 23:24
  • @Wain I don't expect a crash - the table view doesn't necessarily have to reflect the object context. Maybe if it was leaving cells corresponding to deleted objects (which could then be tapped) it might crash, but removing cells for objects which exist shouldn't cause crashes. I've tried reinstalling the app on both physical and simulator devices, always get this problem. I'll try to recreate the model next. Thanks for your suggestions. – Andrew Bennet Jun 02 '16 at 08:35
  • Is your context main queue concurrency type? – pbasdf Jun 02 '16 at 09:09
  • @pbasdf Yes, it is – Andrew Bennet Jun 02 '16 at 09:14
  • 1
    Please show us your FRC initialization code, specifically `predicate` construction part. – bteapot Jun 02 '16 at 09:58
  • And `-controller:didChangeObject:atIndexPath:forChangeType:newIndexPath:` code would be nice, too. – bteapot Jun 02 '16 at 10:15
  • @pbasdf I've added details of the FRC initialization. The predicate construction is done through several helper functions, but ultimately it ends up in `NSCompoundPredicate:orPredicateWithSubpredicates` where my OR predicates are constructed via the following NSPredicate extension: `convenience init(fieldName: String, equalTo: String) { self.init(format: "\(fieldName) == %@", equalTo) }` – Andrew Bennet Jun 02 '16 at 12:44
  • I know that the predicate _should_ be ok, as refetching returns the data I expect. It's just the notifications are wrong. – Andrew Bennet Jun 02 '16 at 12:46
  • Have you implemened a `-controller:didChangeSection:atIndex:forChangeType:` method? – bteapot Jun 02 '16 at 13:20
  • @bteapot Yes, and it is also the same as Apple's "typical use". I have put `print` statements within that, and it doesn't get called when I modify my object (as expected). – Andrew Bennet Jun 02 '16 at 13:22
  • 1
    OK then, maybe this will help: http://stackoverflow.com/a/12514826/826716 . It's about complicated sort descriptors with relationship usage. And in my expirience it also happened with predicates. So, if you can show us your actual predicate and sort descriptors – it may help to pin down the source of this misbehavior. Set the breakpoint at FRC allocation line and `po fetchRequest.predicate` and `po fetchRequest.sortDescriptors`. – bteapot Jun 02 '16 at 13:31
  • Also, is your first sort descriptor set on the same property as FRC's `sectionNameKeyPath`? – bteapot Jun 02 '16 at 13:33
  • @bteapot Printing the predicate and sort descriptors gives: `Optional(readState == "2" OR readState == "1")` and `Optional([(readState, ascending, compare:), (title, ascending, compare:)])` respectively. The first sort descriptor ("readState") is the same as the section key path. The linked question does look very similar, except I am not using any relationships! I have only one entity, with a bunch of attributes, and that's it :/ – Andrew Bennet Jun 02 '16 at 13:44
  • Interesting. I'm almost out of ideas. Can you set the default value of `readState` attribute to `1`, and maybe default value of `title` to some non-nil string in your entity's model? – bteapot Jun 02 '16 at 14:11
  • Another idea. You can try to create objects in temporary MOC, child of `coreDataStack.managedObjectContext`, and push the changes up with `save:` on that temporary MOC. That will ensure that your objects have all attribute values set when FRC can notice changes in its context. – bteapot Jun 02 '16 at 14:16
  • Figured it out! When the fetch request has a comparison between an Int32 and an integer wrapped in quotes, the delegate notifications do not work correctly - despite `resultsController.performFetch()` working fine! If the predicate's format string is changed to `readState == 1` (i.e. without the quotes), the notifications work as expected. Thus it seems to be an inconsistency in how the results controller interprets predicates... Thanks very much for your help with the detective work. I'll update the question and answer in due course, in case anyone else has the same problem. – Andrew Bennet Jun 02 '16 at 19:20

1 Answers1

3

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)
Andrew Bennet
  • 2,600
  • 1
  • 21
  • 55