Search code examples
swiftuitableviewcore-datansfetchedresultscontroller

scrollViewDidScroll called with strange contentOffsetY


I have a UIViewController that holds a NSFetchedResultsController. After the insertion of rows to the top, I want to keep the rows visible as they where before the insertions. This means I need to make some calculations to keep the contentOffSetY right after the update. The calculation is correct, but I noticed that scrollViewDidScroll gets called after it scrolled to my specified contentOffsetY, this results in a corrupted state. This is the logging:

Will apply an corrected Y value of: 207.27359771728516
Scrolled to: 207.5
Corrected to: 207.27359771728516
Scrolled to: 79.5 <-- Why is this logline here?

You can directly clone an example project: https://github.com/Jasperav/FetchResultControllerGlitch (commit https://github.com/Jasperav/FetchResultControllerGlitch/commit/d46054040139afeeb648e1e0b5b113bd98685b4a, the newest version of the project only glitches, the weird call to the scrollViewDidScroll method is now gone. If you fix the glitch I award the bounty. Just clone the newest version, run it and scroll a little bit. You will see strange content offset's (glitches)). Run the project and you will see the strange output. This is the controllerDidChangeContent method:

public func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>) {
    let currentSize = tableView.contentSize.height

    UIView.performWithoutAnimation {
        tableView.endUpdates()

        let newSize = tableView.contentSize.height
        let correctedY = tableView.contentOffset.y + newSize - currentSize

        print("Will apply an corrected Y value of: \(correctedY)")
        tableView.setContentOffset(CGPoint(x: 0,
                                           y: correctedY),
                                   animated: false)
        print("Corrected to: \(correctedY)")
    }
}

If I call tableView.layoutIfNeeded right after the tableView.endUpdates(), the delegate is already called. What does it cause to call the delegate method? Is there any way it does not scroll?


Solution

  • I downloaded your code and made some tweaks to fix the glitching issue. Here is what I have done.

    1. set estimatedRowHeight property of table view to some number
        init() {
            super.init(frame: .zero, style: .plain)
            estimatedRowHeight = 50.0
            register(MyTableViewCell.self, forCellReuseIdentifier: "cell")
        }
    
    1. created a function to handle the UITableView reload action without modifying the current contentOffset
        func reloadDataWithoutScroll() {
            let lastScrollOffset = contentOffset
            beginUpdates()
            reloadData()
            layoutIfNeeded()
            endUpdates()
            layer.removeAllAnimations()
            setContentOffset(lastScrollOffset, animated: false)
        }
    
    1. Updated controllerDidChangeContent function to make use of the reloadDataWithoutScroll function
        UIView.performWithoutAnimation {
            tableView.performBatchUpdates({
                tableView.insertRows(at: inserts, with: .automatic)
            })   
            inserts.removeAll()
            tableView.reloadDataWithoutScroll()
        }
    

    When I execute with these changes, it doesn't scroll when a new row is added. However, it does scroll to show the new added row when the current contentOffset is 0. And I don't think that would be a problem, logically speaking.