Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Table view jumps when scrolling up #17

Open
marcelmika opened this issue Feb 19, 2015 · 16 comments
Open

Table view jumps when scrolling up #17

marcelmika opened this issue Feb 19, 2015 · 16 comments
Labels

Comments

@marcelmika
Copy link

How to recreate:

  1. Scroll down to the bottom
  2. Add a couple of rows by pressing the + button
  3. Scroll up - the table view will start jumping

I have such issue in my own project. I have an infinite feed where I load data dynamically whenever the user scrolls to the bottom. However, when I want to scroll up it jumps and gives you a bad user experience.

Did you guys figured out this issue?

@andriichernenko
Copy link

Have the same problem after navigating to another view controller and back:
navigationController?.pushViewController(UIViewController(), animated: true)

The iOS7 approach still works https://github.com/smileyborg/TableViewCellWithAutoLayout

@lwtwl23
Copy link

lwtwl23 commented Mar 11, 2015

Have the same problem too

@smileyborg
Copy link
Owner

Hey @marcelmika, @deville, & @waterlee23,

Please be sure to file bug reports with Apple about this issue. As far as I know, it is a (fairly serious) bug with self-sizing cells on iOS 8.

Until it's fixed by Apple, there may be nothing we can do as I am not aware of any workarounds for this issue yet.

@lwtwl23
Copy link

lwtwl23 commented Mar 12, 2015

@smileyborg Thanks, I've got the same problem in my own project and thought it was my fault, seems like I can stop finding bugs :)

@rainypixels
Copy link

Did anyone file a bug? It seems like one exists on Open Radar for this, but it'd be good for Apple to see a few more bugs filed on the same issue to help prioritize during triage. FWIW, I have an infinite feed scenario similar to @marcelmika, and this bug is a deal-breaker for that scenario.

From what I can tell, the issue is being caused because height calculation of the new cells added to the bottom of the tableview is being deferred until the cell is about to appear on screen. So when you add 10 new cells to the bottom of the tableview, I suspect that the scrollview height, contentoffset, etc. are all adjusted by 10 * estimatedRowHeight (which is static). This in turn renders the y-positions of all the cells above that point (i.e. the point where you loaded 10 more cells) to be incorrect, and when you scroll up, the layout pass updates them as you scroll leading to lots of sad.

A quick workaround that I've implemented is the age-old cell height cache workaround, and so far, I really like it because it's cell-implementation agnostic and it's very lightweight. Basically:

  1. I maintain an array (you could store this however you like, but an array works for my needs) to store cell heights (i.e. var itemHeights: [CGFloat])
  2. I populate the array with cell heights by grabbing it from tableView:willDisplayCell using cell.bounds.height (i.e. itemHeights[indexPath.row] = cell.bounds.height).
  3. Instead of using the estimatedRowHeight property, I've implemented the tableView:estimatedHeightForRowAtIndexPath method. I simply return the corresponding cell height there (i.e. return itemHeights[indexPath.row])

I'll obviously optimize things a little more, but even this rough workaround makes the tableView scroll like butter. No jittering at all. I'll report back once I've had some time to implement it properly.

@andriichernenko
Copy link

@rainypixels Not sure I understand how your solution works. estimatedHeightForRowAtIndexPath is called for each added cell as soon as they are added. Note that at this point we don't know the exact cell height, because willDisplayCell is yet to be called for it (what should I return then?).

After that, heightForRowAtIndexPath and willDisplayCell are called (in this order) for each displayed cell, and estimatedHeightForRowAtIndexPath is never called for it again. In this case I don't see how returning the exact height from estimatedHeightForRowAtIndexPath helps.

@rainypixels
Copy link

@deville

estimatedHeightForRowAtIndexPath is called for each added cell as soon as they are added.

Actually, isn't it called for all cells whenever reloadData or insertRowsAtIndexPaths is called? That's the behavior I'm seeing, and that's really what we need for a proper calculation of the scrollview contentSize (which is really what's causing the jittering).

After that, heightForRowAtIndexPath and willDisplayCell are called (in this order) for each displayed cell, and estimatedHeightForRowAtIndexPath is never called for it again. In this case I don't see how returning the exact height from estimatedHeightForRowAtIndexPath helps.

Good question; I wasn't clear enough. The one detail I failed to mention is that I initialize the itemHeights array with a sensible height (in my case, UITableViewAutomaticDimension, but you can just as easily use a value like 100.0) for each index. I update this with a real height in willDisplayCell. So whenever cells are added, estimatedHeightForRowAtIndexPath is called for the entire tableView. It returns UITableViewAutomaticDimension (for all cells that were just added) but an actual cell height for previously displayed cells (because we updated the cache when the cells were displayed earlier). Since our real issue is with scrolling back to the top (because those actual cell heights seem to get lost on table updates), this solution works fine because estimatedHeightForRowAtIndexPath returns the correct heights for the previously displayed cells, and thus calculates the scrollview size and offset correctly.

Here's code for a trivial implementation:

// this is the cell height cache; obviously you don't want a static size array in production
var itemHeights = [CGFloat](count: 1000, repeatedValue: UITableViewAutomaticDimension) 

func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
  if itemHeights[indexPath.row] == UITableViewAutomaticDimension {
    itemHeights[indexPath.row] = cell.bounds.height
  }
  // fetch more cells, etc.
}

func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
  return itemHeights[indexPath.row]
}

Yes, there's a small performance hit that grows over time and depends on the complexity of your cells. Once you have hundreds of cells, estimatedHeightForRowAtIndexPath is called hundreds of times (if there's a way to prevent that, I'd love to hear it), and that's a trade-off that only you can make.

Hope that helps. Let me know if I'm missing anything or if it's still unclear.

@bobmoff
Copy link

bobmoff commented Jun 30, 2015

Thanks @rainypixels for your estimatedHeight caching solution it works like a charm.

I have bumped in to a few problems though. They are both related to resizing of cells. I am using the tableview.beginUpdates() + tableview.EndUpdates() trick to animate the height change of a cell. When someone taps a cell I activate another, stronger, constraint that changes the height of the cell. Now the cache needs to be updated somehow, as it has a new height. This one I solved by also updating the cache inside the didEndDisplayingCell method so that when resizing a cell and then scrolling away from it, the new height will be in the cache. This works fine, but the second issue (that I cannot resolve) is when the cell that I am resizing is one of the last rows. When I first expand the cell its fine, but if I scroll down a little after expanding so that I am at the bottom of the tableview and then collapse the cell the animation that occurs first decreases the height of the cell from the bottom up, pulling the lower cells with it and even if there is no more cells to animate up. Then the animation ends with a big glitchy jump back so that the last row is position at the bottom of the tableview again.

Maybe I need to make and example project for this to you to be able to understand :)

@bobmoff
Copy link

bobmoff commented Jul 1, 2015

Hi again. I forked this repo to showcase the problem I tried to describe in the post above.

https://github.com/IMGNRY/TableViewCellWithAutoLayoutiOS8

I have implemented the estimated height cache and when tapping on a cell it will toggle between expanded and collapsed by activating/deactivating a constraint. This works great as long as you're not at the bottom of the tableview (or close to the bottom). Try scrolling down all the way and tap on last cell. It will expand to show all the text i the body label. Now tap on the same cell again to collapse it. The cell collapses, but the y position of the cell is never changed and instead it just shrinks the cell from the bottom up leaving the bottom part of the table view empty (as there are no cells there to display. When the animation is finished the tableview realises that it is scrolled outside of the bounds and snaps back with jumpy glitch. When doing this kind of collapse using the old school 7.0 style, the tableview automatically corrects itself inside the animation and everything looks smooth.

Any idea of how one might solve this?

I have tried updating the estimated height cache manually when collapsing but that didn't change this behaviour.

@bobmoff
Copy link

bobmoff commented Jul 1, 2015

Hmm. I think I have found a solution.

If I switch the animation from UITableViewScrollPosition.None to UITableViewScrollPosition.Middle

Before:

tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.None, animated: true)

After:

tableView.scrollToRowAtIndexPath(indexPath, atScrollPosition: UITableViewScrollPosition.Middle, animated: true)

The glitch goes away. 👍

@rainypixels
Copy link

@bobmoff Whoops! Getting to this too late. Glad I could help with the glitch. :P

Seriously though, that's an elegant solution. I'll keep it in mind if I run into this. Here's to another small win for this self-sizing battle. :)

@speedoholic
Copy link

speedoholic commented Aug 27, 2017

@rainypixels Thanks a ton for your quick fix. Might have saved me hours of UI adjustments and pixel calculations which was the only other solution to avoid the jumps. I am writing this comment to share my following experience while implementing the itemHeights cache:

I have 15 cells, each of them having different UI and elements. When the view loads, the service response is used to populate all the cells and the height needs to be calculated based on certain logics. (used stack views to deal with hiding of particular sections inside a cell). When I started using automatic dimensions, I got the mentioned jump issue even after I tried providing an approximate height in the estimatedHeightForRowAtIndexPath method which was close to the actual height of each cell.

After implementing the above mentioned itemHeights cache, most of the jumps went away but in one particular scenario, the jump was still happening where the cell contained an imageView. While debugging I noticed that the array values are only getting updated once. When I removed the condition which checks if itemHeights[indexPath.row] == UITableViewAutomaticDimension , and implemented the willDisplayCell method as follows, things worked out and the last jump also went away:

func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
    itemHeights[indexPath.row] = cell.bounds.height
}

Thanks again.

@ashalva
Copy link

ashalva commented Sep 29, 2017

The solution really works, thanks for it @rainypixels . I know it is a bit old post, but I will update the solution if one has several sections instead of one.

//Assuming having 100 sections with 100 rows
fileprivate var cellHeights = [[CGFloat]](repeating: [CGFloat](repeating: UITableViewAutomaticDimension, count: 100), count: 100)

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        if cellHeights[indexPath.section][indexPath.row] == UITableViewAutomaticDimension {
            cellHeights[indexPath.section][indexPath.row] = cell.bounds.height
     }
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
        return cellHeights[indexPath.section][indexPath.row]
}

@ashalva
Copy link

ashalva commented Sep 29, 2017

UPDATE:
Instead of using static number of cells or sections here is the dynamic workaround with dictionary.

fileprivate var cellHeights: [Int: [Int: CGFloat]] = [:]

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
        if let dict = cellHeights[indexPath.section] {
            if dict.keys.contains(indexPath.row) {
                return dict[indexPath.row]!
            } else {
                cellHeights[indexPath.section]![indexPath.row] = UITableViewAutomaticDimension
                return UITableViewAutomaticDimension
            }
        }
        
        cellHeights[indexPath.section] = [:]
        cellHeights[indexPath.section]![indexPath.row] = UITableViewAutomaticDimension
        return cellHeights[indexPath.section]![indexPath.row]!
    }

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        if let dict = cellHeights[indexPath.section], dict[indexPath.row] == UITableViewAutomaticDimension {
            cellHeights[indexPath.section]![indexPath.row] = cell.bounds.height
        }
 }

@saidkagirov
Copy link

I do not have this cache solution, I tried not to use automaticDimension because I know the height of the my cell in heightRowForIndexpath I returned number 80.f, with insert sections, I continue to jerk when scrolling up tableView. How to solve? Please, help me @rainypixels, @bbbuka, @speedoholic, @bobmoff

@NarasimhaDo
Copy link

@smileyborg Thanks, I've got the same problem in my own project and thought it was my fault, and one more thing we are not using storyboards, we create tableview cell customized.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

10 participants