UITableView powered by FetchedResultsController with UITableViewAutomaticDimension - Cells move when table is reloaded - ios

Current set up:
TableView with automatically calculated heights:
self.tableView.sectionHeaderHeight = UITableViewAutomaticDimension;
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 152.0;
self.tableView.estimatedSectionHeaderHeight = 50.0;
Whenever the fetched results controller updates its data the tableview is reloaded:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[self.tableView reloadData];
}
The cell is configured using a Xib. The first label is pinned to the top of the cell, each following label is pinned to the top of the label above it and the label at the bottom is pinned to the bottom of the cell.
The Issue:
Each time i set a "Favourite" property on an item in the table view, the fetched results controller is fired to reload the table and the scroll position is changed. It is this change in the scroll position that i am trying to fix.
Additional Info
If i use fixed cell heights it resolves the issue BUT i require UITableViewAutomaticDimension because the first label can wrap over two lines and the remaining labels may or may not be present.
Example
Note - As i select the Fav button it sets the fav property in Core data and reloads the table. Why is the table jumping around?

It happens because of the following sequence:
UITableView initialized and showing 5 cells. Height of each of that cells is known to UITableView. It asks its delegate for exact height before displaying each cell by calling a method -tableView:heightForRowAtIndexPath:.
UITableView scrolled exactly 3 cells from top. Heights of this cells are known to be exactly [60, 70, 90] = 220 summarily. UITableView's contentOffset.y is now 220.
UITableView gets reloaded. It purges all its knowledge about cells. It now still knows its contentOffset.y which is 220.
UITableView asking its data source about general metrics - number of sections and number of rows in each section.
UITableView now beginning to fill its contents. First it needs to know size of its contents to correctly size and position its scroll indicators. It also needs to know which objects - table header, section headers, rows, section footers and table footer - it should display according to its current bounds, which position is also represented by contentOffset. To begin placing that visible objects it first needs to skip objects that falls in invisible vertical range of [0…220].
If you haven't provided values for any of estimated… properties and haven't implemented any of tableViewController:estimated…methods then UITableView asks its delegate about exact height of headers, footers and rows by calling appropriate delegate methods such as -tableView:heightForRowAtIndexPath:. And if your delegate reports the same number of objects and the same heights for them as before reload, then you will not see any visual changes to position and size of any table elements. Downside of this "strait" behavior became obvious when your table should display large number of rows, lets say 50000. UITableView asks its delegate about height of each of this 50000 rows, and you have to calculate it yourself by measuring your text for each corresponding object, or when using UITableViewAutomaticDimension UITableView doing the same measuring itself, asking its delegate for cells filled with text. Believe me, it's slow. Each reload will cause a few seconds of interface freeze.
If you have supplied UITableView with estimated heights, then it will ask its delegate only for heights of currently visible objects. Objects in vertical range of [0…220] are counted by using values provided in estimatedRowHeight or -tableView:estimatedHeightForRowAtIndexPath: for rows and by corresponding methods for section headers and footers. By setting estimatedRowHeight to 60, you telling UITableView to skip three rows (60 * 3 = 180) and to place row 4 with offset of -40 from top visible edge. Hence visual "jump" by 40 pixels up.
A "right" solution here would be not to call reloadData. Reload rows only for changed objects instead, use -reloadRowsAtIndexPaths:withRowAnimation:. In case of NSFetchedResultsController + UITableView use this classic scheme.

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.

UITableView does not handle contentOffset correctly when scrolling/paging

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

How to get height of UITableView placed in another UITableView in Cell as childViewController

I'm struggling with wired kind of problem. I have UITableView with one cell. This cell is only holding containerView with childViewController (second tableView). My problem is that, first tableView(parent) must have UITableViewAutomaticDimension row height but it doesn't work (It dosen't know correct size of that cell with second tableView).
How to get correct size of second tableView) ?
Second tableView (inside Cell) have scrollingEnabled turned off (tableView.scrollEnabled = false), first tableView must have correct size of second tableView in order to provide correct scrolling experience.
Since a UITableView inherits from UIScrollView you can use the contentSize property to get this information. This is how the scrollbar works on the side.
I have done a similar thing before (long ago), so the following is somewhat hazy/ irrelevant/ unessecary. There might be some gotchas with unknown/unrendered cell heights in the embedded tableview. I rememeber having to set the tableview height to a large number (forcing all cells to render) fetching the contentSize then resetting the tableview height to the contentSize.height.
Are you shure you really need second table view? If you have just one cell probably you don't need it. I recommend you to calculate table view height as the summ of it's cells heights. If cells of embedded table view have constant height it could be simple. In other case you also could calculate cells height.

UITableView dynamic cell heights - reload height for single row

I have a UITableView with rows of dynamic heights, many of which contain UITextViews. When the user starts typing in one of the textviews, I have the cells grow to accommodate the size of the textview content using the begin/endUpdates method. Using this method allows the cells to resize without losing keyboard focus on the textview, an important aspect of my app.
However, when I call begin/endUpdates, it reloads the heights for every cell that I have, and I was wondering if there was any way to only recompute the height of a cell at a particular indexPath while the user is typing. I want to do this because my heights for the other cells are expensive to compute as they have dynamic content. I know I could write some height caching code, but I was wondering if there was any method to only recompute the height of a specific cell or set of cells in a UITableView without losing keyboard focus / reloading the data content of that row?
I am using ios8, but will take an ios7/8 solutions.
Create of array of NSIndexPath's. If you have just one indexPath, add just this to the array, and use this method:
[self.tableView reloadRowsAtIndexPaths:[NSArray *array] withRowAnimation:UITableViewRowAnimationAutomatic];
but i think this method, will also hide the keyboard, never tried.
Doesn't seem like this is possible to do for only a single row. I ended up building a caching system for row heights so that at least it is faster to return heights for all cells.

heightForRow not equal cell height in UITableView?

I have created a viewController which displays a plain style UITableView. Sometimes I have enough cells to fill up the table view, sometimes I don't. When I don't have enough cells I want to make the table view smaller, in order to fill the rest of the screen with a view that displays the same background color as the color of my cells.
To do that I multiply the heightForRow with the number of cells that the table view is about to display. I have set the heightForRow to 70 and I have set the height of my custom cells to 70. This does make my table view smaller than even one of my cells.
Is the tableView height calculated in a different way, or what am I doing wrong?
EDIT: The answer below did not answer my question, but it solved my problem.
I use:
[yourtable setTableFooterView:[[UIView alloc] init]];

Resources