I am developing a small app, that includes messenger functionality. When showing a chat thread obviously I used a tableView, populate it with the messages in small bubbles and at some point before viewDidAppear scroll to the bottom, so the most recent messages are shown.
There is a popular solution to scrolling to the bottom of a tableView with scrollToCellAtIndexPath or scrollRectToVisible. These kinda work with little data, and constant cell heights. BUT in my case there is a lot of message data and I use UITableViewAutomaticDimensions for rowHeight. This results in slow loading, not scrolling to the bottom and crazy error messages.
Here's why (IMO): To scroll to the bottom, the tableView has to load all cells because they have automatic dimensions, and can only scroll down after he knows how far it is. Auto layout is another problem. Sometimes it doesn't scroll all the way down, because auto layout didn't finish yet and rowHeight is at the initial value still.
What I tried: putting the scrolling in didLayoutSubviews: solves the problem but loads very slowly, and scrolling is called multiple times (+ some crazy error message)
I guess an upside down tableView would solve the problem, because then the first cell could be the latest message, and no scrolling would be needed.
For optimization, you should use following method:
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat
It is designed for large data sets, because it provides estimates for calculations instead of pixel-perfect size of cells (you also have to provide normal method for getting height - obviously).
Second optimization that you can do is when you have cells that does not change height with time (that is, they have the same height every time), just store already calculated height for each NSIndexPath and then use this value instead of calculating it again. To get height of the cell with AutoLayout, you can use this:
// Configure it
self.configureCell(cell!, indexPath: path)
// Update constraints
cell!.setNeedsUpdateConstraints()
cell!.updateConstraintsIfNeeded()
cell!.contentView.setNeedsLayout()
cell!.contentView.layoutIfNeeded()
// Calculate height of cell
let height : CGFloat = cell!.contentView.systemLayoutSizeFittingSize(UILayoutFittingCompressedSize).height
Related
I'm implementing a chat functionality in my app, and I'm rolling my own UIViewController to do so. I'm using a UITableView instead of a UICollectionView because it fits my needs better.
Some of my messages are text-based (which can be multi-line) and some of them are image-based. I also have some cells in place to show headers (timestamp information) and footers ("Sending...", "Delivered", etc.).
When I build my data source, I am calculating the height needed for each of these cells, and returning that data in my tableView:heightForRowAtIndexPath: method.
I am trying to take more control over this process because automatic sizing of cells using auto layout doesn't work all that well when I want the UITableView to spend most of its time at the bottom, rather than at the top.
So, I have the following needs:
When the view loads, it should start fully scrolled to the bottom
At that point, the UITableView's contentSize should be correctly calculated
Inserting new cells at the bottom of the UITableView should work well and also animate well.
The problem I'm finding is that only some of my cells have their height checked by heightForRowAtIndexPath:.
For instance, here's a particular case:
I have 100 messages + 32 header/footer cells (132 total cells)
In viewWillAppear: (or viewDidAppear:, it doesn't seem to matter), I call this:
int lastRow = (int)(self.dataSource.count - 1);
NSIndexPath *lastPath = [NSIndexPath indexPathForRow:lastRow inSection:0];
[self.tableView scrollToRowAtIndexPath:lastPath
atScrollPosition:UITableViewScrollPositionBottom
animated:animated];
It doesn't change much whether the animated variable is YES or NO. YES seems to call a few more times, but both of them only really call for values at the top and bottom (beginning/end) of my data source. This means that my calculated content height (which is correct) is not in line with self.tableView.contentSize.height.
If, however, I manually scroll up through all of the cells, everything gets sorted out and the UITableView is finally aligned with my calculated height.
In addition to the initial view needing to scroll to the bottom, I also want to be able to add new messages to new cells at the bottom of the UITableView and then animate them into view. That really doesn't work well if I let the UITableView manage its content size and its own animations.
Based on Apple's documentation, I expect it to call heightForRowAtIndexPath: for every cell. I'm getting that from here:
There are performance implications to using tableView:heightForRowAtIndexPath: instead of the rowHeight property. Every time a table view is displayed, it calls tableView:heightForRowAtIndexPath: on the delegate for each of its rows, which can result in a significant performance problem with table views having a large number of rows (approximately 1000 or more). See also tableView:estimatedHeightForRowAtIndexPath:.
So, my basic question is how I can force UITableView to let me set all of these values. I'm using auto layout within the UITableViewCell instances themselves, but I want to have control of cell sizing, rather than letting UITableView do it. Any suggestions?
Just adding tableView.estimatedRowHeight = 0 will solve the problem.
I have a table view over very large amount of data.
For performance reasons, it can't be loaded all at once.
More, sometimes random place of array should be loaded, so incremental pagination is not good option.
The current solution to these requirements is sliding window over data array. As user scrolls, I add cells from one end and remove from opposite end. I use scroll position (by looking at what cells are going onscreen) to determine whether it's time to load new data.
Usually, when you call tableView.deleteRows(at:with:) and remove cells from the beginning of table, tableView adjusts its contentOffset property so user still see same cells as before operation.
However, when tableView is decelerating after scrolling, its contentOffset is not adjusted on updates, and this causes loading new pages over and over until deceleration is completed. Then, on first update after deceleration, contentOffset is fixed by tableView and loading stops.
Same thing occurs when scrolling back and adding values at the beginning of table with tableView.insertRows(at:with:).
How can I make UITableView adjust its contentOffset properly?
OR are there other ways to overcome this bug -- keeping the ability to load arbitrary piece in the middle of data array and scroll from it?
I made a tiny project illustrating the bug:
https://github.com/wsb9/TableViewExample
From your sample project, I can understand is you are trying to implement infinite scroll through the window content concept so that you can always have fixed number of rows (index paths ,lets say 100) so that when window scrolls down/up - table view remove indexPaths from top/bottom accordingly.
And even though you have more data source item you can always have tableView of indexPaths 100
Basically you are dealing with two problem here:
ContentOffset
Dynamic height
Let's assume we have height is fixed (44) and table is not inverted.
To implement Window for infinite scrolling you have to do following:
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let bottom: CGFloat = scrollView.contentSize.height - scrollView.frame.size.height
let buffer: CGFloat = 3 * 44
let scrollPosition = scrollView.contentOffset.y
if (scrollPosition > bottom - buffer) {
dataSource.expose(dataSource.exposedRange.shift(by: 25))
self.tableView.contentOffset.y -= self.dataSource.deltaHeightToRemove
}
}
Decide how much height buffer you need to keep when scroll goes down. This height buffer is the height after which you decides to insert some more item (25) into the datasource.
At this point you now have to remove items from the top
As you remove item from top you are basically telling scrollView to reduce it's content-offset by same height.
In this way total content size will be fixed every time
Hope it will help.
EDIT:-
Here is the modified code which actually does infinite scroll at bottom of
table view with dynamic cell height. This doesn't increase the rows count more than 100. but still loads data in sliding window.
link
From your sample project, I can understand the following,
One thing is you want to increase performance of your table view by only loading few number of cells at a time
Your second concern is sometimes you want to load table view with data that randomly placed in data source array
I checked your code and you have implemented your sliding-window over data-source model very interestingly. The problem caused because of you had been trying to make tableview efficiently by removing and readding cells.
Actually the Dequeuing a cell should be reusing a cell already in memory. Please take a look at the apple documentation,
For performance reasons, a table view’s data source should generally
reuse UITableViewCell objects when it assigns cells to rows in its
tableView(_:cellForRowAt:) method. A table view maintains a queue or
list of UITableViewCell objects that the data source has marked for
reuse. Call this method from your data source object when asked to
provide a new cell for the table view. This method dequeues an
existing cell if one is available or creates a new one using the class
or nib file you previously registered. If no cell is available for
reuse and you did not register a class or nib file, this method
returns nil.
The good news is your sliding-window over data-source model is working perfectly once I removed your row delete and readd mechanism. Here is your working code,
https://drive.google.com/file/d/0B2y_JJbzjRA6dDR3QzRMUzExSGs/view?usp=sharing
I know there are tons of questions about tableViewCell disappearing on scroll, but none of those fit into my situation.
I say disappear, I mean the whole cell disappears, not some of its subviews.
Here's my situation:
I have a tableView with custom cells, each one of them is very complex, some could even host up to three collectionViews in them. Most of these cells are only displayed once, so I cache them. I retrieve data from network, and use it to populate the cells. As sometimes the data may not be fully available, some sections of the cell needs to be hidden: the cell will set some of its collectionViews' height constraint to zero, and isHidden to true, then notify the tableView, who will reload the cells' heights using:
tableView.beginUpdate()
tableView.endUpdate()
I have noticed two behaviors:
1:
If none of the cell's portion is hidden, the cell works perfectly, except when I scroll(from bottom up) too fast, the cell disappears before going out of the bound of the tableView. When I scroll slow, this does not happen.
2:
If some part of the cell is hidden, the cell will disappear before going out of tableView EVEN WHEN I scroll very slowly.
Sorry I cannot post any code, but what I might have done wrong?
P.S.: In my tableView's row height delegate method, I have:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return cellHeights[indexPath.row] ?? 0.1
}
cellHeights is where I use a dictionary to store computed height of the corresponding cell. I am certain that these height values are correct, because when I scroll from top down, the cell looks all right. This only happens when I scroll from bottom up.
I did some experiment with the default height 0.1. When I changed it to 1000, the cells are disappearing randomly even more. 0.1 looks relatively stable, but the problems above still exist.
Thanks.
Update: It looks like the tableView.estimatedRowHeight setting is somehow related to the problem, since the variance of the problem changes as the number changes, and setting it to 9999 makes it "go away" but that seems super hacky. The 9999 also breaks the animations I intended to do on the show/hide. 9999 also breaks the scrollbars funtionality. See the discussion thread below for additional details and full context.
I have a view controller which holds two Container Views. Each container view holds a UITableView controller. They're orientated vertically each taking up the full height.
The container on the right side has a width of 100 and starts off screen with the trailing constraint set to -100 which allows the other container to fill the whole screen initially.
The table on the left has automatic adjusting cell heights using UITableViewAutomaticDimension since when the hidden view is shown, some text on two lines may need to wrap to the next line.
When I change the constraint to be 0 and show the view, if I'm not at the top of the list, rather than expanding what's visible on the screen, it seems to be expanding everything, thus pushing what was on the screen down, further than it should be going.
What's weird though, is subsequent expand/collapses work as expected, only adjusting what is visible.
I've attached a video a video showing the issue, I'm just trying to connect things up and get the sizing working right now, the content in the right view will be different eventually.
The following is what's happening in the video
I expand/collapse from the top of the list and it works as expected
I scroll to middle of list, and expand, and my content gets pushed down further than it should be.
I then collapse and expand it again, and this time it stays in place since no sizes changed on the visible screen.
It doesn't matter how far down the list I go, the same behaviour happens, getting worse as I go further down.
I'm adjusting the constraints like this on a button press for now.
if self.timeContainerConstraint.constant == 0 {
self.timeContainerConstraint.constant = -90.0
} else {
self.timeContainerConstraint.constant = 0.0
}
Does anyone know why the content is jumping when I scroll down the list and expand the first time, but not jumping on subsequent attempts?
As you have variable cell heights, the tableview can't correctly calculate the scroll offset when its frame changes. It has to rely on the estimated row height, but as you have a fixed value for this, the more cells 'above' the current scroll position, the greater the error between the estimated and the actual value.
I was able to solve this by caching the cell heights in order to return a more accurate estimated height using estimatedHeightForRowAt:
var cellHeights = [IndexPath:CGFloat]()
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
let frame = tableView.rectForRow(at: indexPath)
self.cellHeights[indexPath] = frame.size.height
}
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return self.cellHeights[indexPath] ?? 140.0
}
Using this approach, the expansion and contraction of the side bar didn't affect the table's position. I was able to animate the constraint change for a nice smooth reveal and the table didn't 'jump', even if the table was scrolling while the side bar was opened.
My sample project is here
I have a UITableView that reads information from CoreData via the proper mechanisms (using a FetchedResultsController, etc). This information is either textual, or a URL to a local image to load into the tableview.
Data needs to be populated in the table in a bottom-up fashion (similar to a messaging app). I am targeting iOS 8+, but if I use estimatedHeightForRowAtIndexPath, I get terrible jerkiness on 3+ multi line labels and images. The estimate seems way too far off unless it's a one line UILabel. My hunch is that the cell height is being estimated in a top down manner, such that cell heights are growing from top of cell to bottom of cell. This means that scrolling top to bottom is fine, but bottom to top is not, since the cell is being resized "downward" dynamically as I scroll upward.
I am currently using heightForRowAtIndexPath to calculate cell heights. The problem with this is that it takes a very long time for the view to initially load because cell heights are all calculated at once. I am using cell height caching to store cell height so that once the view has loaded, scrolling is buttery smooth.
So my question is this: how do you use heightForRowAtIndexPath without taking the 3-5 second initial load hit?
And follow up bonus question, is there any way to reliably use estimatedHeightForRowAtIndexPath when you have cells that are vastly different in height? We're talking anywhere from 44px to 300px. From what I've read, I can't use the estimatedHeight calculation at all in this situation.
I've exhausted all of the stackoverflow posts concerning estimatedHeight/heightForRowAtIndexPath and I'm now starting to look at the same posts more than once. So I'm stuck.
why woncha stuff a few rows in the table to populate the visible area and after
the viewDidAppear start stuffing older messages on top of the table one or two
at the time with animation none, automatic or whatever.
this way with the postponement of the uitableview population
me thinks you'd get a passable performance.
or you could do it the skype way, postponing population of the table
with older messages until after table bounces off the top edge.