NSFetchedResultsController not updating

Discussion in 'iOS Programming' started by grandM, Jan 13, 2016.

  1. grandM, Jan 13, 2016
    Last edited: Jan 13, 2016

    grandM macrumors 6502a

    grandM

    Joined:
    Oct 14, 2013
    #1
    I have a split view Controller. Upon tapping on a cell in the mainVC the data is shown in the detailVC. When I want to change data in the detailVC the editVC is fired up. Data is changed and saved to CoreData in the editVC. Now comes the problem: upon change in the editVC my tableView (in the mainVC of a splitViewController) is not adapted. In my detailVC the information is shown correctly. When I perform a reloadData() of the tableView in mainVC it shows the information correctly in the tableView. I do not want to do so because that reloadData() is expensive. Must I conform editVC to the NSFetchedResultsControllerdelegate? Basically it comes down to the question how I can make my NSFetchedResultsController act upon changes in my editVC? Note the data is adapted on the original managedObjectContext. So I do not have to merge managedObjectContexts as I only have one.
     
  2. FelixII macrumors member

    FelixII

    Joined:
    Feb 22, 2013
    Location:
    Germany
  3. grandM thread starter macrumors 6502a

    grandM

    Joined:
    Oct 14, 2013
    #3
  4. FelixII macrumors member

    FelixII

    Joined:
    Feb 22, 2013
    Location:
    Germany
    #4
    NSFetchedResultsController connects you core data objects with indexes in your UITableView. You can use NSFetchedResultsControllerDelegate to get notified when your core data objects change. However you still have to update your UITableView to reflect these changes, e.g.:

    Code:
    - (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
        switch (type) {
            case NSFetchedResultsChangeInsert: {
                [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
                break;
            }
            case NSFetchedResultsChangeDelete: {
                [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
                break;
            }
            case NSFetchedResultsChangeUpdate: {
                [self configureCell:(TSPToDoCell *)[self.tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
                break;
            }
            case NSFetchedResultsChangeMove: {
                [self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
                [self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
                break;
            }
        }
    }
    
    - (void)configureCell:(TSPToDoCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    // Fetch Record
    NSManagedObject *record = [self.fetchedResultsController objectAtIndexPath:indexPath];
    // Update Cell
    [cell.nameLabel setText:[record valueForKey:@"name"]];
    [cell.doneButton setSelected:[[record valueForKey:@"done"] boolValue]];
    }
    
    
    Source: http://code.tutsplus.com/tutorials/core-data-from-scratch-nsfetchedresultscontroller--cms-21681

    To update an existing cell, you can either set the new values manually like in the example above or call the method I've linked in my first post (reloadRowsAtIndexPaths:withRowAnimation). The advantage of my approach is that you can animate the change.
     
  5. Mascots macrumors 65816

    Mascots

    Joined:
    Sep 5, 2009
    #5
    No, you need to have NSFetchedResultsController tell the Table View to manipulate the individual rows itself. That splits the responsibilities: the FRC only watches the fetch request, allowing whatever to listen to its signals. In this case, the table view controller (or whatever controller managing your table view) should be the object listening and applying the changes.

    When an entity that is managed in your FRC is updated, it sends the appropriate delegate callbacks explaining the changes to the data which, as it has it, seamlessly integrates into table view functions. As stated by Felix, you need to use the reloadRows, insertRows, and deleteRows supplied by tableview, along with nesting the batch of changed between a beginUpdates and endUpdates. There's quite a few documents online with prefabbed code, but you may need to change it depending on how you want things to happen (the reason it is decoupled!)
     
  6. grandM thread starter macrumors 6502a

    grandM

    Joined:
    Oct 14, 2013
    #6
    I did notice that when I first called out for the beginUpdates and then ended the code with the EndUpdates the tableView adjusted. However the sections weren't added.
    --- Post Merged, Jan 13, 2016 ---
    I have this code in my masterVC. When I use beginUpdates and EndUpdates the cells are adjusted but the sections aren't?
     
  7. Mascots macrumors 65816

    Mascots

    Joined:
    Sep 5, 2009
    #7
    Look at the link I sent you in the previous post. There is a section titled, "Integrating the Fetched Results Controller with the Table View Data Source" and I suggest you read that in full.
     
  8. grandM thread starter macrumors 6502a

    grandM

    Joined:
    Oct 14, 2013
    #8
    I think there is a misunderstanding. I read all texts. To be clear: my mainVC conforms to the
    NSFetchedResultsControllerDelegate. It implements the methods
    Code:
     
    func controllerWillChangeContent(controller: NSFetchedResultsController) {
    
            self.tableView.beginUpdates()
    
        }
    
    
        func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
    
            switch type {
    
                case .Insert:
    
                    self.tableView.insertSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
    
                case .Delete:
    
                    self.tableView.deleteSections(NSIndexSet(index: sectionIndex), withRowAnimation: .Fade)
    
                default:
    
                    return
    
            }
    
        }
    
    
        func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
    
            switch type {
    
                case .Insert:
    
                    tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
    
                case .Delete:
    
                    tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
    
                case .Update:
    
                    self.configureCell(tableView.cellForRowAtIndexPath(indexPath!)!, atIndexPath: indexPath!)
    
                case .Move:
    
                    tableView.deleteRowsAtIndexPaths([indexPath!], withRowAnimation: .Fade)
    
                    tableView.insertRowsAtIndexPaths([newIndexPath!], withRowAnimation: .Fade)
    
            }
    
        }
    
    
        func controllerDidChangeContent(controller: NSFetchedResultsController) {
    
            self.tableView.endUpdates()
    
        }
    
    The tableView is filled with information gathered by the FRC. The problem is that upon changing the values of an object using the same moc the tableview isn't reflecting those changes. I'm passing that moc and the object to editVC, edit it and save it back to CoreData using that moc. Hence the tableView ought to be edited?
     
  9. FelixII macrumors member

    FelixII

    Joined:
    Feb 22, 2013
    Location:
    Germany
    #9
    Did you set breakpoints to check if your delegate methods are being called correctly and retrieve the updated model objects?
     
  10. Mascots, Jan 13, 2016
    Last edited: Jan 13, 2016

    Mascots macrumors 65816

    Mascots

    Joined:
    Sep 5, 2009
    #10
    Sorry, I'm not sure since you're describing multiple issues here: individual cells aren't reloading and sections aren't reloading, but it doesn't sound as if your getting consistent behaviors and we can't be aware of your changes so I'm stabbing at the dark. Prior, it sounded as if you had only skipped conforming to controller(_:didChangeSection:atIndex:forChangeType: ):

    Though, from the last post, it sounds like this: You pass your object and MOC to your edit controller, make changes, then call save on the MOC. The issue seems to be that the FRC either isn't getting the notification that changes were made OR it isn't informing your table view of updates.

    Two questions: Are you sure your are setting the FRC delegate to your table view controller? Second, are you sure you are passing the same MOC? A FRC will disregard notifications from a even a child that is derived from its own MOC.

    Edit: Agreed. Use breakpoints, find out if your FRC delegates are even being called and work from there.
     
  11. grandM thread starter macrumors 6502a

    grandM

    Joined:
    Oct 14, 2013
    #11
    The moc seems to be the same
    Code:
    my moc in MASTER is <NSManagedObjectContext: 0x79577c30>
    my moc in insert is <NSManagedObjectContext: 0x79577c30>
    my moc in EDIT is <NSManagedObjectContext: 0x79577c30>
    
    In my masterVC I have following code:
    Code:
    let aFetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.managedObjectContext!, sectionNameKeyPath: nil, cacheName: "Master")
    
            aFetchedResultsController.delegate = self
    
    sectionNameKeyPath is something else but nil in my code referring to CoreData
     
  12. Mascots macrumors 65816

    Mascots

    Joined:
    Sep 5, 2009
    #12
    Hmm. Did you try and set a breakpoint to ensure your delegate methods are being called?

    One thing that instantly pops into my head is that your FRC isn't being retained and is being released after creation, though I doubt that is the case since it doesn't sound right (I safely assume that you retain it to pull the information in your cellForIndexPath).

    The very last suggestion I have besides manually digging through the stack to see what is happening is to set nil to your FRC cache - I can't find any information but there's a ringing in the back of my head that there's a bug related to caching, but I don't have any details off hand and what you're doing here is straight forward.
     
  13. grandM thread starter macrumors 6502a

    grandM

    Joined:
    Oct 14, 2013
    #13
    I set breakpoints (blue in the gutter). The app didn't stop so I presume they are not being called?
    --- Post Merged, Jan 13, 2016 ---
    I set the cachename to nil but it didn't help
    --- Post Merged, Jan 13, 2016 ---
    But it must have something to do with the FRC not being notified the object is changed. The object is changed for sure as when in detailVC the data on the object got changed.
     
  14. Mascots macrumors 65816

    Mascots

    Joined:
    Sep 5, 2009
    #14
    Correct. So you know that the FRC isn't firing when the MOC changes, but I'm sorry as that's about as far as I can guide you in debugging without having access to more of the information and unless any one else has any pointers or suggestions.

    The only thing I can suggest at this point is to ensure that your FRC is being retained and isn't having its delegate reassigned. The chances that you're coming across a thread abnormality seem slim to none and there isn't an apparent issue with your delegate callbacks themselves, other than their lack of being called.
     
  15. grandM thread starter macrumors 6502a

    grandM

    Joined:
    Oct 14, 2013
    #15
    Must editVC conform to NSFetchedResultsControllerDelegate?
     
  16. Mascots, Jan 13, 2016
    Last edited: Jan 13, 2016

    Mascots macrumors 65816

    Mascots

    Joined:
    Sep 5, 2009
    #16
    Nope. The Edit Controller should have no knowledge of any FRCs which contain the object is is editing - only the MOC which the object is being edited in. When you set a delegate on an FRC, it is registered to receive notifications of changes from that MOC (a la Apple):

    Basically, the FRC has no interaction with anything other than the delegate it sends commands to. Under the hood, the FRC subscribes to NSNotificationCenter and waits for a MOC to post NSManagedObjectContextObjectsDidChangeNotification, then ensures that the calling MOC is the same as the one it is gathering data from, if objects in its local graph have changed, and explains how they changed.

    That's why I am having a hard time believing your FRC is being retained. It should always be reporting changes with its fetched items in the same MOC to the delegate
     
  17. grandM thread starter macrumors 6502a

    grandM

    Joined:
    Oct 14, 2013
    #17
    I might have found the problem. A source pretends "NSFetchedResultsController does not seem to track changes when the predicate contains a key path that drills down into a relationship." In my case that looks like what I'm doing. I have relationships in CoreData. Say I have a variable Woman = the managedObjectContext. This object has a relationship looks. Looks connects the Entity Woman with the Entity Looks. Looks has attribute haircolor and again a relationship pointing to Figure. Figure has attributes waist and length. Could this drilling down cause the problem?
     
  18. grandM thread starter macrumors 6502a

    grandM

    Joined:
    Oct 14, 2013
    #18
    I made progress. It's like I supposed. NSFetchedResultsController does not report a change when this change occurs in the drill down. I put a boolean on my Woman. When I change the boolean the tableView is reloading. Unfortunately the section is not adapting yet.
    I'm looking at two possibilites. The first would be to put the section attribute as a direct attribute of Woman. The second option would be to make changes to the Woman, copy her and re-insert her. What would be the cheapest option?
     
  19. Mascots macrumors 65816

    Mascots

    Joined:
    Sep 5, 2009
    #19
    Ah, I apologize as I should have suggested that sooner. You are spot on: A FRC only looks at its immidiate graph, not down the tree.

    I did something to work around it in an app, but will need to look back at notes, previous code, or the web to refresh my mind. I'll keep you posted (sorry I didn't confirm last night when I glanced through the thread! I bet this is killer).
     
  20. grandM thread starter macrumors 6502a

    grandM

    Joined:
    Oct 14, 2013
    #20
    No sweat. I'm glad you are helping out. I can confirm it is the problem. I moved the attribute to a first level attribute of the object: everything works as it ought to. I'm only a bit in doubt if I would stick to this move (which puts information at a place one wouldn't expect it to be) or changing the object, deleting it and adding it again.
     
  21. Mascots macrumors 65816

    Mascots

    Joined:
    Sep 5, 2009
    #21
    Recycling the object like that would be pointless, as you've mentioned. Your best bet may be to fire a KVO notification through the relationship that tricks the FRC into thinking that one of its observed entites changed so it triggers a reload on that indexpath.
     
  22. grandM thread starter macrumors 6502a

    grandM

    Joined:
    Oct 14, 2013
    #22
    any idea how this ought to be done? I know that KVO means something like setValue forKey but how would this trigger a reload?
     
  23. Mascots, Jan 14, 2016
    Last edited: Jan 14, 2016

    Mascots macrumors 65816

    Mascots

    Joined:
    Sep 5, 2009
    #23
    I just noticed the link you posted before and see they are suggesting the same thing, so I'll do my best to try and translate so you can understand what you need to do. Let me know if I'm unclear since I'm making metaphoric pictures here.

    In your case, you are displaying the value of hairColor which is a property on the relationship of looks. When you are displaying the Woman entity in a row, you are essentially superimposing hairColor as if it were a property directly on the Woman entity. We can visualize this superimposition through a computed property, which doesn't have data but gathers it elsewhere via its getter, and in this case is something like:

    Code:
    var hairColor: UIColor {
      get {
        self.looks.hairColor
      }
    }
    Now, the issue comes from changing the value of hairColor. When updated, the entity Looks is actually the only object that changed, and because that object isn't observed by FRC there are no updates even though Woman superimposes that variable. The FRC doesn't have a way to be told that any random object change relates to Woman since Woman is the object extending the graph outside of the FRCs knowledge.

    So, our goal is to let the graph know that changing the property hairColor on Looks actually affects its children. This can be done two ways depending on how extensive relationships affect their children.

    The first and easiest, and the method I would suggest for you, is to write a custom setter on the Looks entity that the property is changing on, a la:

    Code:
    var hairColor: UIColor? {
            set {
                self.willChangeValueForKey("rawHairColor")
                self.setPrimitiveValue(ConvertColorForDBStore(newValue), forKey: "rawHairColor")
                self.didChangeValueForKey("rawHairColor")
         
                self.willChangeValueForKey("woman") //Notify the graph that the woman relationship will change
                self.didChangeValueForKey("woman") //It didn't, BUT it triggers KVO as if it had
            }
            get {
                self.willAccessValueForKey("rawHairColor")
                let color = ConvertDBStoreToColor(self.primitiveValueForKey("rawHairColor"))
                self.didAccessValueForKey("rawHairColor")
                return color
            }
        }
    (Take this as reference, I wrote it on my phone so I'm pretty sure it won't compile :S)

    There's a few things to take into consideration, but I believe it will get my point across. First, that I don't override or use property observers - this is because you cannot override NSManaged properties and it makes the API for normalizing values before storing into the database extremely clean. The magic in this snippet is the KVO that fires on the (one-to-one assumed) relationship - it tells the graph that the relationship property needs to be reevaluated.

    In the grand scheme of things, that object on the other side of the Look relationship is Woman and it will be marked dirty and therefore be registered by the FRC as needing an update.

    I'll hint at the second method but won't go into detail: Instead of having the ManagedObject subclass mark its relationship as dirty, you can have an object subscribe to Notification Center to listen for NSManagedObjectContextObjectsDidChangeNotification and manually move though the graph to mark the entities which relate to the changed entity as dirty. This makes more sense when dealing with deeper relationship nests and especially when working with self-referencing relationships.
     
  24. grandM thread starter macrumors 6502a

    grandM

    Joined:
    Oct 14, 2013
    #24
    Problem with my initial solution is I'm breaking MVC. It works though. Problem with your computed properties solution is I'd have to write this setter for each attribute. Or am I mistaken? I have a lot of nesting. How does the second solution work?
     
  25. Mascots, Jan 14, 2016
    Last edited: Jan 14, 2016

    Mascots macrumors 65816

    Mascots

    Joined:
    Sep 5, 2009
    #25
    Breaking MVC? How?
    MVC is more about separation of responsibilities and in my example, your model is only responsible for telling itself that additional pieces of it are dirty - this already happens when setters and getters are managed by Core Data (via willChangeValue and didChangeValue) but is scoped to the single property. There's a thread where I recently talk about the dynamic keyword in Objective-C, and Core Data uses this (with NSManaged) to create the setter and getter within the managed subclass instance but defined out of the superclass.

    Bigger picture, this prevents scope creep of the FRC since it continues to only react to the changes on its own object set as it would if you were using Core Data's generated functions in any other case. If I were to present a solution that required you to amend the FRC to observe more properties than its initial fetch request, I'd be a little more wary about misaligning responsibilities because things begin to get sloppy very quickly as that would require redefining those relationships & dependencies outside of your model for a use case.

    Ideally, you should be informing each child in the relationship of a change to a property in the parent only if the changed property affects the child. In this case, hairColor clearly affects Woman but something like hairProduct could not, so you wouldn't want the parent (Looks) to mark its child (Woman) as dirty since there is no noticeable change to the woman object. This means that you must manually be aware of and anticipate how changes affect other objects, but also means that you are not firing KVO operations willy-nilly (could result in craziness, instability, or just redundant processing).

    --

    As I've stated before, the second method I presented to you will need to be handled and conceived entirely by you per your design. I can't really describe more beyond this and maybe a small snippet:

    Essentially, when that notification is fired, you're given a MOC and list of objects which have changed. You'll need to find which changed objects affect others, then do a little logic to ensure that any other objects that should react on that changed object are affected, and finally mark them as dirty through KVO accessing. This makes it easier to manage a relationship like:
    Woman <-> Looks <-> HairColors​
    Since an update to HairColors (e.g., a title property from "blond" to "blonde") is acknowledged by Woman regardless of the objects in the graph that are separating the two.
     

Share This Page