Poor initial load performance with heightForRowAtIndexPath and UITableView scrollToBottom - uitableview

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.

Related

heightForRowAtIndexPath not called for every cell

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.

Dynamic cell height issue with UITableViewCell autolayout jerk while scrolling

I am trying to do something like loading up different type of cells with custom height in a uitableview. The tableview cells are subclassed and consists of labels with the respective constraints. Each cell is having a dynamic height.
Now even before my table reloads the data, I am calculating the height that is required for the resizing of the cells and caching it in my model class so that I dont have to calculate the height when the data is rendered on the device.
To calculate height i did use the tutorial from Ray Wenderlich and I am having the right set of heights applies to the objects.
Now the problem comes. Whenever I am dequeueing the cells there is a
kind of a small jerk that gives me an indication that my cell is
dequeued while scrolling.
How can i make these movement smooth so that there is no jerk while scrolling the view ?
The height is getting assigned in and does get the value as per the current type of data getting loaded.
estimatedRowForIndexPath
Also I am calling layoutIfNeeded from my cellForAtindexPath
Suggestions are most welcome.
It's very hard to say without seeing your code in cellForRowAtIndexPath, and without seeing your cells and their respective code. Here are some general questions I would investigate:
What is the content of the cells and how complex is the view hierarchy in the cell?
Even though you are supplying the correct estimated height, an autolayout pass still needs to happen, and a complex view hierarchy will take time to resolve
Does the cell contain images?
Images that need to be decompressed from a file (UIImage imageNamed:) can be intensive and cause scrolling issues, check images are not bigger than they need to be. If needed, bump this work onto a background thread.
Are you calling a complex method to configure the cell for display in cellForRowAtIndexPath?
Look at the work actually being done in cellForRowAtIndexPath, is there a complex method being triggered in you cell subclass or view model?
Are you adding and removing views to the cell view hierarchy in cellForRowAtIndexPath?
If views are being added, removed, created, inflated from a xib, constrained etc during the cell config, this could slow things down. Try to do only what is strictly needed. Check if there is any code being run internally in the cell subclass during cellForRowAtIndexPath that could be moved to cells initWith... or awakeFromNib methods (ie code that could just run once when the cell is created, rather than every time the cell is displayed)
Also run the Instruments time profiler, see if that offers any more clues

Keeping reuse capabilities of UITableView when it is nested in a UIScrollView

To implement a rather intricate design of a screen in an iOS app, I have a UITableView nested inside of a UIScrollView.
To keep the logic simple, I implemented a method on the UITableView that calculates its entire height, and i use the result of that method and set a constraint on the nested table view, so that the scrolling logic can be solely on the UIScrollView to deal with. (I forward methods such as scrollRectToVisible from the UITableView to the UIScrollView)
While this works great with small data sets, I have recently discovered the the reuse capabilities of the UITableView are not used, because the framework believes the entire UITableView to be visible when I set that height constraint. A simple log method in the cellForRowAtIndexPath method shows all cells get calculated at once.
My question is, is there anything I can do where I would be able to tell the nested UITableView how much of it is actually visible on screen, and to only compute those visible cells?
I basically need to override whatever part of UITableView that is responsible for calculating what cells should be visible on screen.
The table view will think of itself as filling its whole frame with cells. If you limit the height it will limit the cell count visible. Are you using the deque with reuse identifier method (if not see below)
How can I recycle UITableViewCell objects created from a XIB?

Re-Enabling dequeuing on a UICollectionView within a UIScrollView

I'm trying to set up a particularly large UICollectionView, and I'm embedding it within a UIScrollView (as per this answer to another question on SO) in order to support scrolling in both directions. I've got it up and running, everything is working perfectly, except that performance takes a HUGE hit as the size of the CollectionView grows.
After some hunting around, I was able to figure out the problem: because that SO answer calls for the CollectionView to be resized so that all the cells are visible at once, "dequeuing" isn't a thing anymore. The CollectionView within the ScrollView has dimensions of 2000x2000 (or whatever), so even though the ScrollView is just the size of the screen and only shows 20 cells or so at a time, the CollectionView thinks all cells are visible and needed right now. Thus, every cell in the CollectionView is generated at once, meaning the page takes a ludicrously long time to appear.
Is there a way to communicate to the CollectionView which cells are currently visible through the ScrollView, so it doesn't generate unnecessary cells and dequeues them as normal? Or if not, is there a way to make a CollectionView that scrolls diagonally, but that doesn't have this problem?

Issues regarding dynamic resizing of label and row heights (iOS)

Context:
Building an app that populates a table that takes in data from a asyc json dump.
The cells are of a custom class (I defined). The main label in the cell can be very long.
It is "placed" in storyboard within a prototype cell but customized via code (pretty standard stuff).
Labels are resized in cellForRowAtIndexPath and rows are resized via heightForRowAtIndexPath -- rows are resized by forcing a call to cellForRowAtIndex like Massimo's answer here
So per the question at hand - I've noticed some interesting (bad) things that happen.
First issue: When the table loads, the rows and labels are dynamically resized correctly! Great! However, when I scroll down and then scroll back up, the label heights will be incorrect -- (for example) the first row was correct at loading. Then when I scroll down and then scroll back up to see it again, it will be truncated. Specifically, the row size will be fine but the label height will change and become truncated to 2 lines only. Wondering if this is because I did both storyboard and coding to customize the cell. Anybody see this before?
Second issue: When I scroll down, while the rows are sized correctly (large), the labels are short (truncated.) Wondering if it's some reverse of the above "potential answer".
"potential answer" is that the rows are all calculated and stored "up front" so that scrolling down/then back up doesn't affect it. However, when cells go "out of view" and are dequeued then when they re-viewed (scroll down/then back up) it will rely on the storyboard.(inappropriately?)
All three of your issues are symptomatic of returning the wrong height in heightForRowAtIndexPath. In my data model classes I have a calculateHeight method that I call in heightForRowAtIndexPath. The model also caches the answer so it doesn't have to recalculate it after the first call. The cell class uses the model's calculated height to layout its subviews.
"ANSWERED" by deleting the prototype cell from the storyboard and making them fully in code, the issue went away. The fundamental workings are still not understood (ie. the interactions between storyboard vs. code when cells are put queued and then viewed again)

Resources