Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Prevent/Postpone tableview updates from NSFetchedResultsController when table is not visible

Xcode 11.0

Swift 5.1

I'm working on a "simple" app using Core Data.

The single Core Data entity is named Topic and comprises Title (String), Details (String) and isFavorite (Bool)

I'm using a custom UITabBarController with 3 tabs - Random Topic, Favorite Topics, and All Topics

Favorite Topics and All Topics are UITableViews sharing the same subClass, TopicsViewController.

Using custom TopicTabBarController I set a property in TopicsViewController, based on the tab, that determines whether or not a predicate is used with the FetchRequest for the two tabs sharing this controller.

This works as expected, even with FRC cache (I'm very new to Swift!)

If a topic is favorited on any view, the tableViews in both Favorite Topics and All Topics update to reflect that change. This is precisely how I want it.

The trouble is I'm getting a warning about the tableView being updated while not visible:

UITableView was told to layout its visible cells and other contents without being in the view hierarchy (the table view or one of its superviews has not been added to a window).

I set a symbolic breakpoint at UITableViewAlertForLayoutOutsideViewHierarchy but making sense of that is a bit above my head at the moment.

I tried returning early from the FRC Delegate methods if the view wasn't loaded but that was clearly not the solution. Like I said, this is all new to me.

I thought setting the FRC Delegate to nil and back to self as suggested in this post would help, but that only prevents the managedObjectContext from saving (at least that's what I'm seeing with Navicat). I can step through the code and see that the proper instances of the controller class are being manipulated based on the custom property I set for the predicate.

Here's the related code for that part:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    self.fetchedResultsController.delegate = self
    self.performFetch()
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    self.fetchedResultsController.delegate = nil
}

// MARK:- Helper methods
func performFetch() {

    do {
        try fetchedResultsController.performFetch()
    } catch {
        fatalCoreDataError(error)
    }
}

Here's where the entity is saved, which is not happening with the above code in place:

@objc func toggleFavorite(_ sender: UIButton) {
    let buttonPosition = sender.convert(sender.bounds.origin, to: tableView)
    if let indexPath = tableView.indexPathForRow(at: buttonPosition) {
        let topic = fetchedResultsController.object(at: indexPath)
        topic.isFavorite = !topic.isFavorite
        try! managedContext.save()
    }
}

Here's a Gist with more details. I find it much easier to read this way.

I also just came across this post on the Apple Dev forum. Looks like the same issue, unresolved.

I honestly don't know if that's too much or too little information. Was really hoping I'd stumble on the solution while trying to explain the problem.

Thanks for your time.

like image 658
Mark Avatar asked Sep 27 '19 00:09

Mark


1 Answers

For what it's worth, I was getting the same warning and I was able to fix it by doing something very similar to what you tried. Basically the only difference is the tableView.reloadData().

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    fetchedResultsController.delegate = self
    tableView.reloadData()
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    fetchedResultsController.delegate = nil
}

... though I would think that performing a fetch would work just the same?

like image 107
Jim Rhoades Avatar answered Nov 19 '22 07:11

Jim Rhoades