0

TLDR - my FRC appears to get out of sync with the table view it is linked to. How can I get it to sync?

My View Controller (VC) has a table view called holesTable that is managed by a fetched results controller (FRC) called fetchedResultsController.

When the user presses the save button on VC, it calls the IBAction called SaveHoles. This saves the data in the FRC's managed object context then tests that all data rules have been followed. Any errors are caught and a message is sent on to another VC which displays the error to the user after an unwind (not shown here).

I'm discovering that the FRC doesn't have the same contents as the on-screen holesTable because the function lookForSIProblems doesn't pick up any errors when it called. However, I am able to validate that the underlying database has received and stored the data that's on the screen.

Subsequent calls to the VC will show the saved data and subsequent presses of the Save button will find the error problems.

So, it appears that my FRC results are out of sync with what is shown in holesTable at the time I do the validation.

Here's the salient code:

fileprivate lazy var fetchedResultsController: NSFetchedResultsController<Hole> = {
    // Create Fetch Request
    let fetchRequest: NSFetchRequest<Hole> = Hole.fetchRequest()

    // Configure Fetch Request
    self.teeColourString = self.scorecard?.value(forKey: "teeColour") as! String?
    fetchRequest.predicate = NSPredicate(format: "%K == %@ AND %K == %@", "appearsOn.offeredAt.name", self.courseName!, "appearsOn.teeColour", self.teeColourString!)

    fetchRequest.sortDescriptors = [NSSortDescriptor(key: "holeNumber", ascending: true)]

    // Create Fetched Results Controller
    let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: self.coreDataManager.mainManagedObjectContext, sectionNameKeyPath: nil, cacheName: nil)

    // Configure Fetched Results Controller
    fetchedResultsController.delegate = self

    return fetchedResultsController
}()

@IBAction func saveHoles(_ sender: Any) {

    print("Prepare to save")

    do {
        try fetchedResultsController.managedObjectContext.save()
    }
        catch let error as NSError {
            print("Could not save. \(error), \(error.userInfo)")
    }

    lookForSIProblems(holes: fetchedResultsController.fetchedObjects!)
}

func lookForSIProblems(holes: [Hole]) {
    // Takes a fetched result set of 18 records and looks for duplicate or missing SI values
    // It should not be possible to have duplicate hole numbers, so the check is done to see if the SI number has already been seen before and, if so, for which other hole.

    var SIexists: [Bool] = [false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false, false]
    var SIToCheck: Int
    let maxHole = 18
    var currentHole = 0
    var proceed = true

    while currentHole < maxHole && proceed {

        SIToCheck = Int(holes[currentHole].strokeIndex)

        if SIToCheck == 0 {
            // This can only happen when the scorecard has not been completed

            messagesToPassBack = ["Incomplete Scorecard", "Not all holes have been given a valid Stroke Index. Your changes have been saved but you cannot enter player scores until this error has been fixed."]

            flagIncompleteParForCourse(errorCode: 0)

            proceed = false

        } else if !SIexists[SIToCheck-1] {
            // No SI for this hole number has yet appeared, so set it and carry on
            SIexists[SIToCheck-1] = true
            currentHole += 1
        } else {
            // This SI has already been seen

            messagesToPassBack = ["Duplicate Stroke Index", "Stroke Index \(SIToCheck) has been duplicated. Your changes have been saved but you cannot enter player scores until this error has been fixed."]

            flagIncompleteParForCourse(errorCode: 1)

            proceed = false
        }
    }
}

EDIT #1 -

In response to the comment from @AgRizzo, the full FRC delegate methods are shown below.

These appear to be working well and is a carbon copy of delegate code I've used elsewhere in my app.

extension ScorecardViewController: NSFetchedResultsControllerDelegate {

    func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        holesTable.beginUpdates()
    }

    func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
        holesTable.endUpdates()
    }

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {

        switch type {
        case .update:
            holesTable.reloadRows(at: [indexPath!], with: .automatic)
        case .insert:
            holesTable.insertRows(at: [newIndexPath!], with: .automatic)
        case .delete:
            holesTable.deleteRows(at: [indexPath!], with: .automatic)
        case .move:
            holesTable.moveRow(at: indexPath! as IndexPath, to: newIndexPath! as IndexPath)
        }
    }

    func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType) {

        switch type {
        case .insert:
            holesTable.insertSections(NSIndexSet(index: sectionIndex) as IndexSet, with: .fade)
        case .delete:
            holesTable.deleteSections(NSIndexSet(index: sectionIndex) as IndexSet, with: .fade)
        case .move:
            break
        case .update:
            break
        }
    }

}

EDIT #2:

To see if the FRC was updating on each edit, I added a call to lookForSIProblems into the FRC delegate methods thus:

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {

    switch type {
    case .update:
        holesTable.reloadRows(at: [indexPath!], with: .automatic)
        lookForSIProblems(fetchedResultsController.fetchedObjects!)
    case .insert:
        holesTable.insertRows(at: [newIndexPath!], with: .automatic)
    case .delete:
        holesTable.deleteRows(at: [indexPath!], with: .automatic)
    case .move:
        holesTable.moveRow(at: indexPath! as IndexPath, to: newIndexPath! as IndexPath)
    }
}

And the result is that the FRC is showing values that are in sync with the table. I went on to isolate where I think the problem is. If I edit a fetched object in the table then immediately hit the Save button, the FRC in lookForSIProblems seems to be using 'dirty' data. However, if I edit the object in the table but click on another item in the table, say, then the FRC in lookForSIProblems uses 'fresh' data.

Not sure what this means.

Dave
  • 71
  • 10
  • Generally speaking *out of sync* between a tableView and frc is due to the functionality found in the frc.delegate object which you have not shown. I believe your problem would be found in your code that conforms to the [NSFetchedResultsDelegate protocol](https://developer.apple.com/documentation/coredata/nsfetchedresultscontrollerdelegate) – AgRizzo Aug 23 '17 at 14:31
  • Hi @AgRizzo, I don't think the FRC delegate methods are the problem. Anyway, I've included the code for these as an edit to my post. – Dave Aug 23 '17 at 14:35
  • *If I edit a fetched object in the table then immediately hit the Save button, the FRC in lookForSIProblems seems to be using 'dirty' data.* - Could this simply be a timing issue? Maybe you are accessing your FRC before the saved changes are propagated through whatever notifications exist between managed object context changes, the FRC comparisons that need to happen and then the delegate functions. I assume you can prove that your changes eventually get propagated from the cell edits, a save and then the delegate functions – AgRizzo Aug 24 '17 at 22:17
  • @AgRizzo - you are almost certainly correct. The changes do propagate into the FRC and it does appear that by inspecting the FRC at a later point, I find what I am looking for. So, what options do I have to wait? Does the `performAndWait()` on the FRC apply here? – Dave Aug 28 '17 at 11:35
  • Why not apply your logic directly to the managed object context (as opposed to the FRC)? I would also try to move `lookForSIProblems` to outside your VC. This provides advantages such as reducing your controller size and making the code more reusable. I don't fully understand all of your code, but if you have a Hole entity, do you have a `Course` entity (or a `Scorecard` entity) that consists of 18 Holes? I would make the validity check part of the `Course` model. (Then this will help you implement unit tests.) – AgRizzo Aug 28 '17 at 13:12

0 Answers0