This question already has answers here:
reloadData() of UITableView with Dynamic cell heights causes jumpy scrolling
(22 answers)
Closed 4 years ago.
I need to expand/collapse a section of UITableView. I do this by calling reloadSections:withRowAnimation: and returning 0 from tableView:numberOfRowsInSection: if the section is supposed to be collapsed.
It works fine except for one case. If the table view is scrolled all the way down and content size decreases after collapse, it ends up being scrolled beyond maximum offset.
Now instead of scrolling in place with animation it just snaps there, which looks ugly:
Here's what I have tried:
Wrapping reloadSections:withRowAnimation: in beginUpdates/endUpdates:
tableView.beginUpdates()
tableView.reloadSections(indexSet, withRowAnimation: .Automatic)
tableView.endUpdates()
That did not help.
Wrapping reloadSections:withRowAnimation: in CATransaction with completion block, calculating the scroll distance within it and scrolling table view with animation:
CATransaction.begin()
CATransaction.setCompletionBlock {
// tableView.contentSize is incorrect at this point
let newContentOffset = ... // calculated value is not correct
tableView.setContentOffset(newContentOffset, animated: true)
}
tableView.beginUpdates()
tableView.reloadSections(indexSet, withRowAnimation: .Automatic)
tableView.endUpdates()
CATransaction.commit()
How do I make sure the table view does not jump?
I ran into this issue when using custom headers in collapsible sections. I did solve it by implementing.
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat
Related
I have a problem with reusing a cell.
In my cell there is a view that changes the constraints depending on its state. By default, cell height = 0, when the user clicks on the button, it changes its constraints top, leading, trailing and bottom. The problem is that if I expand, press expand view in one cell, scroll down (or up), then another cell will also be expanded.
I change the variable responsible for uncovering the cell in the prepareForReuse method, and it doesn't help :(
My solution used to be like this: I redid the constraints in the configure method (it is called in the cellForRow method in VC), and it worked, BUT there were lags when scrolling, apparently, because I change the constraints every time a cell is configured and reused
Are there ways to avoid lag when scrolling? Is there another way to do this?
I use SnapKit for make layout.
I would try avoiding making the cell have a height of zero. This may mess with table view and create an overhead in multiple situations. Since cells are dequeued to reduce resource consumptions you should not produce zero-sized cells as theoretically you can see infinite number of such cells in screen. So basically you can easily have a situation where hundreds of cells are being processed by your table view which user is not even seeing.
An alternative to zero-height cell is to insert/delete cells from table view. There are even animations for that already in place. To achieve this you need to correct your numberOfRows method and your cellForRowAt where both of them need to ignore "hidden" cells. Beside that you basically just need something like
func hideCell(indexPath: IndexPath) {
tableView.beginUpdates()
self.items[indexPath].isHidden = true
tableView.deleteRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
}
func showCell(indexPath: IndexPath) {
tableView.beginUpdates()
self.items[indexPath].isHidden = false
tableView.insertRows(at: [indexPath], with: .automatic)
tableView.endUpdates()
}
This should fix most (all) of your issues. But if you still want to stay with having small/zero sized cells there are other ways to fix your issue.
When you change your constraints you should also call (maybe you already do that but did not post it)
cell.remakeParentCommentConstraints()
tableView.beginUpdates()
tableView.endUpdates()
and make sure that when cellForRow is being called you also call cell.remakeParentCommentConstraints() because as cell is dequeued you can not know what state the previous cell was in.
Even when doing all of this correctly you may experience jumping. This is now because estimated height logic is not very smart by default and you need to improve it. A quick fix is to cache your cell heights:
private var cellHeightCache: [IndexPath: CGFloat] = [:]
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeightCache[indexPath] ?? 44.0
}
func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
cellHeightCache[indexPath] = cell.bounds.height
}
The part with 44.0 should be whatever you have currently set as estimated row height.
This trick may get a bit more complicated if you insert/delete rows (as in first solution). As you also need to insert/remove heights from cache. It is doable, just not as easy.
I solved the problem by dividing the states into different cells
The ScrollToRow is not working as I want.
The function I wrote seems to be working fine, but I want it to scroll down to last row only when its added like it happens in chat. When you get a new message, your tableview scrolls to the bottom to read new message.
However, my tableview seems to reload every time before performing scrollToLastRow function (what I mean is every time before scrolling to bottom... it first goes back to top first cell and starts scrolling to bottom from there Every time when a new cell is created).
My code is simple for the scroll:
func scrollToLastRow()
{
let indexPath = IndexPath(row: messages.count - 1, section: 0)
self.chat.scrollToRow(at: indexPath, at: .bottom, animated: true)
}
To check my code:
here's a link to my repository
ChatApplication
the ViewController's used for chat is ---> chatVC
the ViewController used to make chat is -> chatCell
I have tried anything I could get all over I could find.
Tried dispatch (without it the scroll function wasn't working at all)
Used the function on many different places (including viewDidAppear)
Used dispatch.asyncAfter for delay.....
but nothing worked.
#Fahad, you are reloading the tableview before the scrollToLast function is getting called. Please check and please verify at your end.
Note please update your GitHub repository url in your question properly as it not clickable.
Use this function for scrolling to the bottom.
func scrollToBottom() {
let bottomOffset = CGPoint(x: 0, y: contentSize.height - bounds.size.height + contentInset.bottom)
setContentOffset(bottomOffset, animated: true)
}
use tableview.scrollToBottom()
Use tableView.insertRows(at: [IndexPath], with: UITableView.RowAnimation) for adding object to end of the tableView.
From the problem you say and looking in to your code, the animation issues are due to reloading the entire tableView after a new item is added to dataSource array,
What you have to do is to use Call tableView.insertRows(at: [IndexPath], with: UITableView.RowAnimation) with the correct indexPath on your tableView and the tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) will be invoked.
I have a tableview. I reload a section. My sectionheader has a line that requires to be drawn.
The positioning of this line and how it’s drawn is based on contentview’s frame
If if do tableview.reloadData() it get’s drawn correctly. Tableview header’s frame is non-zero
If I do notesTable.reloadSections([1], with: .automatic) it doesn’t get drawn correctly. Tableview header’s frame is zero!
( I need to use reloadSections because I want to animate it. reloadData() doesn’t give any animation)
so a very watered down example of my viewForHeaderInSection is:
func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
// some code
sectionView.setupUI()
return sectionView
}
Header class:
class SectionHeaderclass: UITableViewHeaderFooterView{
func setupUI(){
let triangleViewArea = ViewWithTriangleLine(triangleCenter:Double(contentView.frame.width - 30))
}
}
👆 contentView.frame.width returns 0 if I animate the section reload. Why?! How can I fix this?
If you want to update header, and it has some drawing view, its better you call setNeedsDisplay() on headerView instead of reloading tableview.
Note that reloadSections(...) updates cells in the sections not the header view.
I tried many ways to solve this problem, but I couldn't. My tableView jumps after it loads more data. I call the downloading method in willDisplay:
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let lastObject = objects.count - 1
if indexPath.row == lastObject {
page = page + 1
getObjects(page: page)
}
}
and insert rows here:
func getObjects(page: Int) {
RequestManager.sharedInstance.getObjects(page: page, success: { objects in
DispatchQueue.main.async(execute: {
self.objects = self.objects + objects
self.tableView.beginUpdates()
var indexPaths = [IndexPath]()
for i in 0...objects.count - 1 {
indexPaths.append(IndexPath(row: i, section: 0))
}
self.tableView.insertRows(at: indexPaths, with: .bottom)
self.tableView.endUpdates()
});
})
}
So what do I wrong? Why tableView jumps after inserting new rows?
I have just find the solution to stop jumping the table view while
inserting multiple rows in the table View. Am facing this issue from
last few days so finally I resolved the issue.
We have to just set the content offset of table view while
inserting rows in the table view. You have to just pass your array of
IndexPath rest you can use this function.
Hope so this method will help you.
func insertRows() {
if #available(iOS 11.0, *) {
self.tableView.performBatchUpdates({
self.tableView.setContentOffset(self.tableView.contentOffset, animated: false)
self.tableView.insertRows(at: [IndexPath], with: .bottom)
}, completion: nil)
} else {
// Fallback on earlier versions
self.tableView.beginUpdates()
self.tableView.setContentOffset(self.tableView.contentOffset, animated: false)
self.tableView.insertRows(at: [IndexPath], with: .right)
self.tableView.endUpdates()
}
}
I had a similar problem with tableView. Partially I decided this with beginUpdates() and endUpdates()
self.tableView.beginUpdates()
self.tableView.endUpdates()
But this didn't solve the problem.
For iOS 11, the problem remained.
I added an array with the heights of all the cells and used this data in the method tableView(_:heightForRowAt:)
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath.row] ?? 0
}
Also add this method tableView(_:estimatedHeightForRowAt:)
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath.row] ?? 0
}
After that, the jumps stopped.
First, check your tableView(_:estimatedHeightForRowAt:) - this will never be accurate but the more likely the cell height ends up with this estimate the less work the table view will do.
So if there are 100 cells in your table view, 50 of them you are sure will end up with a height of 75 - that should be the estimate.
Also it's worth a while noting that there is no limit on the number of times the table view may ask its delegate of the exact cell height. So if you have a table view of 1000 rows there will a big performance issue on the layout out of the cells (delays in seconds) - implementing the estimate reduces drastically these calls.
Second thing you need to revisit the cell design, are there any views or controls whose height need to calculated by the table view? Like an image with top and bottom anchors equivalent to some other view whose height changes from cell to cell?
The more fixed heights these views/ controls have the easier it becomes for the table view to layout its cells.
I had the same issue with two table views, one of them had a variable height image embedded into a stack view where I had to implement the estimate. The other didn't had fixed size images and I didn't need to implement the estimate to make it scroll smoothly.
Both table views use pagination.
Last but not least, arrays are structs. structs are value types. So maybe you don't want to store any heights in an array, see how many copies you're making?
calculating the heights inside tableView(_:heightForRowAt:) is quite fast and efficient enough to work out really well.
Because your loop runs from 0 to objects count:
for i in 0...objects.count - 1 {
indexPaths.append(IndexPath(row: i, section: 0))
}
The indexpaths generated counting for row 0 till object's count. and hence the rows are getting added at top of table (i.e. at row 0) and hence causing tableview to jump as you are there at bottom of tableview.
Try changing range as:
let rangeStart = self.objects.count
let rangeEnd = rangeStart + (objects.count - 1)
for i in rangeStart...rangeEnd {
indexPaths.append(IndexPath(row: i, section: 0))
}
Hope it helps..!!!
I have simple UITableViewController.
View for header. Which inits from xib.
And single type of cell.
After deleting cell with swipe, cell which above deleted one become visible above HeaderView, when other cells just hides below HeaderView as it should be.
If something above not clear - ask.
Video: https://youtu.be/aX-iPnM3q4Q
I think this should solve it:
tableView.sendSubview(toFront: tableView.headerView(forSection: 0)!)
(assuming you have only 1 section)
You can set this after deleting the cell, for example in commitEditingStyle.
Found the solution.
func deleteItem(from indexPath: IndexPath, tableView: UITableView) {
cellsData.remove(at: indexPath.row)
CATransaction.begin()
tableView.beginUpdates()
CATransaction.setCompletionBlock {
tableView.reloadData()
}
tableView.deleteRows(at: [indexPath], with: .left)
tableView.endUpdates()
CATransaction.commit()
}
tableView.reloadData() do the thing that fixing appearing cell above the headerView, but kills animation. So if you don't care about animation you can add only reloadData().