0

On selecting a row in an NSFetchedResultsController (FRC), my app allows the details to be changed by segueing to a detail view controller. After saving changes, the edited row is returned and inserted in the FRC.

The .update code is triggered in the following switch:

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
    switch type {
    case .update:
        configureCell(cell: tableView.cellForRow(at: indexPath! as IndexPath)!, indexPath: indexPath! as NSIndexPath)
        tableView.reloadRows(at: [indexPath!], with: .automatic)
    case .insert:
        tableView.insertRows(at: [newIndexPath!], with: .automatic)
    case .delete:
        tableView.deleteRows(at: [indexPath!], with: .automatic)
    case .move:
        tableView.moveRow(at: indexPath! as IndexPath, to: newIndexPath! as IndexPath)
    }
}

The configureCell() method is:

func configureCell(cell: UITableViewCell, indexPath: NSIndexPath) {
    var handicap: Float

    let cellIdentifier = "PlayerTableViewCell"

    guard let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath as IndexPath) as? PlayerTableViewCell else {
            fatalError("The dequeued cell is not an instance of PlayerTableViewCell")
        }

    // Populate cell from the NSManagedObject instance
    guard let player = fetchedResultsController.object(at: indexPath as IndexPath) as? Golfer else {
        fatalError("Unexpected Object in FetchedResultsController")
    }

    let firstName = player.value(forKey: "firstName") as! String?
    let lastName = player.value(forKey: "lastName") as! String?
    let fullName = firstName! + " " + lastName!
    handicap = player.value(forKey: "exactHandicap") as! Float

    cell.playerFullName?.text = fullName

    cell.playerPhoto.image = UIImage(data: player.value(forKey: "photo") as! Data)

    cell.numExactHandicap.text = "\(Int(handicap))"
    cell.numStrokesReceived.text = "\(handicap.cleanValue)"

    print("Object for configuration: \(player)")
}

And the cell class definition is:

class PlayerTableViewCell: UITableViewCell {

    //MARK: Properties
    @IBOutlet weak var playerFullName: UILabel!
    @IBOutlet weak var playerPhoto: UIImageView!
    @IBOutlet weak var exactHandicapLabel: UILabel!
    @IBOutlet weak var strokesReceivedLabel: UILabel!
    @IBOutlet weak var numExactHandicap: UILabel!
    @IBOutlet weak var numStrokesReceived: UILabel!

   override func awakeFromNib() {
        super.awakeFromNib()
        // Initialization code
    }

    override func setSelected(_ selected: Bool, animated: Bool) {
        super.setSelected(selected, animated: animated)

        // Configure the view for the selected state
    }

}

-- EDIT #1 --

override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

    // Table view cells are reused and should be dequeued using a cell identifier
    let cell = tableView.dequeueReusableCell(withIdentifier: "PlayerTableViewCell", for: indexPath)

    configureCell(cell: cell, indexPath: indexPath as NSIndexPath)

    return cell
}

The behaviour I get is unexpected in a number of ways.

  1. The on-screen data does not update following the processing of the .update code, retaining the pre-edited values in the FRC.

  2. Only when I select the same row in the table does the row refresh. I.e. it shows the old values but then changes to the new at the instant I select the row again, prior to launching the edited row again.

  3. When I select any row, except row 0, for the first time it sets all values in the cell to the default values shown in the storyboard. However, row 0 never changes prior to the segue under any circumstances.

Any insights very welcome!

-- EDIT #3 [Apr' 20, 2017] --

I've been chipping away at this issue for some time. The core issue remains - The on-screen data does not refresh following the processing of the .update code, retaining the pre-edited values in the FRC.

The key code segments now look like this:

func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?) {
    switch type {
    case .update:
        golferView.reloadRows(at: [indexPath!], with: .automatic)
    case .insert:
        golferView.insertRows(at: [newIndexPath!], with: .automatic)
    case .delete:
        golferView.deleteRows(at: [indexPath!], with: .automatic)
    case .move:
        golferView.moveRow(at: indexPath! as IndexPath, to: newIndexPath! as IndexPath)
    }
}

func configureCell(cell: UITableViewCell, indexPath: NSIndexPath) {
    var handicap: Float
    var player: Golfer

    let cellIdentifier = "PlayerTableViewCell"

    guard let cell = golferView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath as IndexPath) as? PlayerTableViewCell else {
        fatalError("The dequeued cell is not an instance of PlayerTableViewCell")
    }

    // Populate cell from the NSManagedObject instance
    player = fetchedResultsController.object(at: indexPath as IndexPath)

    let firstName = player.value(forKey: "firstName") as! String?
    let lastName = player.value(forKey: "lastName") as! String?
    let fullName = firstName! + " " + lastName!
    handicap = player.value(forKey: "exactHandicap") as! Float

    cell.playerFullName?.text = fullName

    cell.playerPhoto.image = UIImage(data: player.value(forKey: "photo") as! Data)

    cell.numExactHandicap.text = "\(Int(handicap))"
    cell.numStrokesReceived.text = "\(handicap.cleanValue)"

    print("Object for configuration: \(player). And cell values are = \(String(describing: cell.playerFullName?.text)), \(String(describing: cell.numExactHandicap.text)), \(String(describing: cell.numStrokesReceived.text))")
}

I have verified that the controller() method is firing correctly and that the .update switch is firing reloadRows. This in turn fires configureCell() where the contents of the returned row from the FRC are correct and the values inserted into the cell match that returned from the FRC. It just won't refresh the UI.

Dave
  • 71
  • 10
  • 1
    Are you sure your update code is being called? Also, you shouldn't need to call both `configureCell` and `reloadRows`. And what do you mean by "the edited row is returned and inserted in the FRC". Is that a new row being created (not an update)? – Dave Weston Feb 28 '17 at 00:57
  • Hi Dave. In order: 1. Yes, the `.update` code is being called. When I set a breakpoint there, it stops and allows me to step through. 2. If I comment out the call to `reloadRows` then the behaviour changes. Rather than show the original, pre-edited row contents, it now resets the row’s contents to the default storyboard values. Calling `reloadRows` populates the table with the out of date row, not the updated values I want. 3. I mean that I want the edited details to be reflected as an update to the original row in the FRC. – Dave Feb 28 '17 at 10:40
  • 2. I meant you should delete the `configureCell` method call, because that will automatically be called by reloading that row. – Dave Weston Feb 28 '17 at 18:08
  • In your `controller(_:didChange:at:for:newIndexPath:)` method, one of the parameters is the new object itself (`anObject`). If you examine that object, does it have the correct values? Can you share your `tableView(_:cellForRowAt:)` method? – Dave Weston Feb 28 '17 at 18:12
  • Hi Dave. To your points: 1. I deleted the `ConfigureCell` call and it was redundant, as you suggested. 2. Yes, the value of `anObject' contains the values that I set prior to the segue, so that handoff is working OK. 3. I've included the code for the `tableView(_:cellForRowAt:)` method as an edit in the original question. Again, thanks. – Dave Feb 28 '17 at 22:48
  • When you print `anObject` that is correct. I see you also print the object in the `configureCell` method. Do they match? When you print, if you haven't overridden the `description` property, you should also get a hexadecimal value which is the object's location in memory. Are those the same? It's strange that if you just call configureCell that you get the storyboard default. I'm not sure why those cases result in different behaviors. Hmmmm – Dave Weston Mar 01 '17 at 02:46
  • Yes, the `print()` from both points produces the identical object info - including the object id in hex. So, another piece of data; I located a sample app by Bart Jacobs [https://github.com/bartjacobs/RespondToUpdatesUsingTheNSFetchedResultsControllerDelegateProtocol] that implements functionality to what I'm trying to do here. I refactored my code to use the same approach as his app and the odd behaviour in my app is _still_ apparent. This suggests the code in this area might be correct but the underlying data store might be out of sync somehow. Any thoughts? – Dave Mar 01 '17 at 09:46
  • It sounds to me like the data store is working fine and there's something wrong in setting up the cell. It seems straight-forward though, so I'm not sure what could be going wrong. What data are you changing? Can you step through it and see where exactly it goes wrong? If you can share the project, I'm happy to look at it in more depth. – Dave Weston Mar 01 '17 at 23:06
  • The app, as it stands, is only changing a single row in a table. I am unable to determine the exact point it goes wrong as I cannot determine the user event that is happening. The first sign of an issue is the displayed row changes when the user selects a row - at that point the row's contents change to the defaults on the storyboard. After that, any changes made via the detail view controller are only seen by going back into the detail. No other update of the master table occurs. The project is at [https://drive.google.com/file/d/0B1IRETa6Jq7zVmVBTzlFMVc1SFE/view?usp=sharing]. – Dave Mar 02 '17 at 10:53
  • @DaveWeston - excuse the mess in the code. I've commented out a ton of code that I've refactored or experimented with. Many thanks for the help. – Dave Mar 02 '17 at 10:55
  • No problem. I know it's frustrating when you're stumped on a bug. I'll check it out later today. – Dave Weston Mar 02 '17 at 17:04
  • @DaveWeston, I've been off working on a different part of my app since I posted my question. I am now back looking at this and wondered if you had taken a look at my code? I have been unable to make any headway on this myself, unfortunately. – Dave Apr 15 '17 at 15:17

1 Answers1

1

Whilst investigating a memory usage issue, I stumbled upon a duplicate call to dequeueReusableCell which, when fixed, resolved the issue I've been having.

In my post, dequeueReusableCell is called within configureCell. It is also called immediately before a call to configureCell in the override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell call.

I'm now back on track!

Dave
  • 71
  • 10