Scroll UITableView to bottom when using estimated row heights - ios

I'm having a problem that seems like it shouldn't really be a problem. I'm trying to create a comments ("chat") view for my app, but I'm using estimated row heights and can't find a nice way to have the comments start at the bottom, such that when the view is loaded, the latest comment is at the bottom of the screen, just above the input area also at the bottom of the screen.
I've been looking around for ages and have found solutions like these:
[self.tblComments scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
Doesn't work nicely because it uses the estimated row height, which isn't always right (some comments will be much longer).
[self.tblComments setContentInset:UIEdgeInsetsMake(self.tblComments.bounds.size.height - self.tblComments.contentSize.height, 0, 0, 0)];
Same problem as above (contentSize is determined by estimatedRowHeight).
I'm sure there's a perfect solution to this somewhere since so many apps have views like this, I just can't find it. I'd love any insight into something I may be missing here...

Edit:
contentSize is simply "The size of the content view." But since you've set a value for estimatedRowHeight, all your off-screen cells are assumed to be of height estimatedRowHeight.
According to the docs, here's the point of estimatedRowHeight:
Providing a nonnegative estimate of the height of rows can improve the performance of loading the table view. If the table contains variable height rows, it might be expensive to calculate all their heights when the table loads. Using estimation allows you to defer some of the cost of geometry calculation from load time to scrolling time.
So basically, in order to save time, the table is making an assumption about row height and thus content size based on your estimatedRowHeight. By using estimatedRowHeight you're asking your UITableView to make these "shortcuts" in order to improve performance. You're basically telling the program not to calculate the row heights ahead of time, so in order to get the row heights and content sizes of any off-screen content, you have to calculate them manually. As long as you decide to set an estimatedRowHeight, this is unavoidable.
My guess is that many chat apps you've looked at don't use estimatedRowHeight in order to avoid this issue...
But if you do in fact feel as if using estimatedRowHeight helps with your app's performance, here are two suggestions.
SUGGESTION #1
Perhaps it makes sense to have a totalChatHeight variable where you keep track of the total row heights as they come in, calculated using similar logic as your heightForRowAtIndexPath: method. By originally calculating this info in the background as the comments load then incrementing as you go, you'd still maintain the performance benefits of using estimatedRowHeight without sacrificing the functionality of having the table know its row heights.
By doing this, you can structure your contentOffset statement like so to scroll to the bottom using the known table row height information:
CGPoint bottomOffset = CGPointMake(0, totalChatHeight - self.tblComments.frame.size.height);
[self.tblComments setContentOffset:bottomOffset animated:YES];
SUGGESTION #2
Another suggestion would be to calculate your row average dynamically so you have a more precise measurement for estimatedRowHeight and the contentSize of your UITableView would approximately be the same with or without using estimatedRowHeight. That way estimatedRowHeight is actually the estimate row height and the estimated content size should also be near the actual contentSize, so this standard contentOffset formula should work:
CGPoint bottomOffset = CGPointMake(0, self.tblComments.contentSize.height - self.tblComments.frame.size.height);
[self.tblComments setContentOffset:bottomOffset animated:YES];
This could potentially be dangerous though as both the chat log and rounding error grow.
SUGGESTION #3 <-- Perhaps the best way to go in this case
This third suggestion may be the easiest to implement in your case as it doesn't require any ongoing calculations:
Just set estimatedRowHeight to the last row's height before reloading the table's data.
You can calculate that new row's height using whatever algorithm you use in heightForRowAtIndexPath: then set estimatedRowHeight to that height; that way you can use this statement to scroll to the last cell
[self.tblComments scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
and even though, as you said, it determines the row position based on estimatedRowHeight, it should scroll to the correct position as long as the estimatedRowHeight equals the bottom row height.

Related

How to display large UICollectionView with autosizing cells in performant manner?

I am trying to display a UICollectionView capable of displaying a user's photo library where the photo aspect ratio is respected (constrained by the width of the device - think the instagram full width feed view).
I have used examples from UICollectionView Self Sizing Cells with Auto Layout and https://medium.com/#wasinwiwongsak/uicollectionview-with-autosizing-cell-using-autolayout-in-ios-9-10-84ab5cdf35a2 (there is a PR in the repo linked in the article to update the project for iOS 12 FYI) to setup a UICollectionView with variable height cells, but all of these are super laggy/terrible to use when there are a large amount of cells in the view.
For example, the sample app (with iOS 12 fix) here: https://github.com/tttsunny/CollectionViewAutoSizingTest/pull/5 works great with 100 cells, but if you try to display 30,000 then the app is unusable - it gets stuck scrolling and is not responsive. The latency is in the estimated size logic of the UIViewController.
Is using an estimated size not recommended for large size UICollectionView objects? What can I do to get the cells to have different height values then?
While I can't speak to all large collection views, if you are seeing all your latency in the estimated size calculation, then sizing your cells a different way is likely to help a lot. Since you have a fixed width in this scenario, and simply need to calculate the height for each cell, you can accomplish all your size calculations off the main thread, which'll hopefully stop your scrolling getting suck and unresponsive.
More practically, for each "page" of images (I'd recommend starting with 20, but you can adjust this based on how it works in your situation), calculate the required cell heights for each. Do this in a background queue, by looking at the UIImage imageSize, and getting your cell height with: (imageSize.height / imageSize.width) * UIScreen.main.bounds.size.width. You'll need to keep these in memory, probably best alongside the images themselves, and then your sizeForItemAtIndexPath method can just index into that data structure for the cell's size. Then as your user scrolls, calculate the sizes for the "next page" before they get there (using either the collectionView prefetching API or simply hooking into the UIScrollView delegates.
You could even do this calculation in sizeForItemAtIndexPath directly, but calculating things in the background would be even more efficient. Calculating directly in this method could be an easier first step, though :).

What are the effects of setting estimatedRowHeight in UITableView?

It seems that whatever I set in estimatedRowHeight, there isn't any visual differences. Performance wise, I'm not sure how setting a value such as 1 vs 100 makes any difference. The document just says setting a non-negative will give you a better performance, but doesn't elaborate more.
In order to perform its full initial internal layout and thus paint the scroll indicators correctly, the table view must know the heights of all the rows (as well as any section headers etc.).
By supplying the estimatedRowHeight, you allow the table to perform a full initial internal layout knowing only the heights of the visible rows (because it uses the estimated height for all the other rows). For a table view with variable row heights, that's much, much faster.
(Of course there is no point to the estimatedRowHeight if the table view's rowHeight is in fact the height of every row.)

How does estimatedRowHeight affect performance?

When use dynamic UITableViewCells height calculation via UITableViewAutomaticDimension you must set estimatedRowHeight. Is there any information about how does bad estimated value affect performance?
Estimated row height is not as important as you necessarily think. It is mainly used by the table to configure certain UI elements like it's scroll bars for instance. When tableView.reloadData() is called the table recalculates this anyway. As a general rule I just use an estimated row height of either 44 or whatever the height of my placeholder cell that is displayed whilst I am loading data is. The performance repercussions of using an inaccurate estimatedRowHeight are minimal to non-existent as it does not actually contribute to calculating the dynamic height of cells marked as UITableViewAutomaticDimention.

UITableViewCell's rectForSection returns estimate when section not visible

I'm currently trying to add infinite scrolling to a UITableView, which contains a number of calendar events. Since the event title doesn't always fit inside a single line I've added a multi-line UILabel to the cell. In order to calculate the height of the cell I'm taking advantage of UITableView's new self-sizing cells via Auto Layout in iOS 8. WWDC Session 226 talks about this in more detail.
In order to implement the infinite scrolling mechanism I'm overriding layoutSubviews where I need to calculate the height of a section which at the time given ist not visible on screen. This can be done by using [self rectForSection:0]. When doing so the table returns a height based on the estimated row size I had to define inside the table's initialiser in order to make self-sizing cells work.
self.estimatedRowHeight = 44.0;
self.rowHeight = UITableViewAutomaticDimension;
When the section gets on screen I get the correct size of the section, but since I have to update the table's contentOffset based on the computed height of that specific section this causes my table to jump up and down.
Any ideas on how to solve this? Is there a way to force a section to return the actual height and not the estimated one?

Modifying a UICollectionViewFlowLayout to change cells positions

I need to slightly change the way FlowLayout draws cells.
I need to set the collection to scroll horizontally and I want to draw cells from left to right, moving to the next line when the width is completely filled and taking into account pagination.
Here some images to clarify my question:
This is the current behaviour of the FlowLayout:
this is what I'd like to obtain:
Some notes:
I need paginationEnabled = YES
I want to fix the rows to a maximum of 2
I have a fixed number of cells too.
Is there a way to achieve this behaviour working with a UICollectionFlowLayout? Or is this the case to create a totally custom UICollectionLayout?
I believe you would have to manually fill the cells checking the indexpath.row and also checking the width of each cell and self.view.frame.size.width

Resources