Force UITableView or UICollectionView to load cell on screen - ios

From the question the answer seems just reloadData.
This method starts the process that makes the tableview reload data from tableview from numberOfSections to -cellForRowAtIndexPath.
The fact is that most of those methods are launched probably in the next runloop or CATransaction.
I'm trying to get the full size of the tableview to show all the cells thus avoiding the need of scrolling.
The tableView knows its content size only when it load the heightForRowAtIndexPath or the cells, so it seems to impossible to ask the table view for its contentSize right after -reloadData.
It seems useless also to invalidate its intrinsic content size and force auto layout, or asking a -sizeToFit until it loads all the cells it doesn't know its size. Tried also with +systemLayoutSizeFittingSize:giving UILayoutFittingExpandedSize with no success.

The whole point of table and collection views is that you do not have to load all the data at once but recycle the UI elements and only load what is visible. That is the purpose of the datasource approach.
To add an item at the end, just change your data source to have one more that is returned from numberOfRowsInSection or numberOfItemsInSection. Make sure your data source also has content to populate this data point.
You can then reloadRowsAtIndexPaths:, reloadItemsAtIndexPaths: reloadSections: etc. methods of the datasource protocols to reload these data points, but if they are not visible that might not even be necessary.
If, on the other hand, you have a custom UIScrollView, your data source should have the heights or widths of your cells / subviews. You can then easily calculate the position of any new subview to be added.

If you want to get the right content size of your tableView, than you can calculate it yourself using heightForRowAtIndexPath:.
E.g. if cell's height is static, i.e. its never changes, then just simply multiply number of your rows with that height. Else, if you cell's height is dynamic, then you can do following:
- (CGSize)tableViewContentSize
{
CGFloat contentHeight = 0;
for (int i = 0; i < numberOfSections; i++) {
for (int k = 0; k < numberOfRows; k++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:k inSection:i];
contentHeight += [self tableView:self.tableView heightForRowAtIndexPath:indexPath];
}
}
CGSize contentSize = CGSizeMake(self.tableView.frame.size.width, contentHeight);
return contentSize;
}

Related

How to keep cell's content on screen while scrolling until the cell runs out of available space?

The questions itself does not say much, but what I am trying to say is: how can one move the cell's content (let say a UILabel), so the label can be seen on screen while the collection view is being scrolled, until the collectionView's cell runs out of available space.
But, a picture is worth a thousand words:
I have something like this, a UICollectionViewCell with horizontal scroll.
Normally, when I scroll this is what it will happen:
But, what I would like to achieve is the names in the firsts cells to scroll to the right while there is space available in the cell, without forgetting that when there's no more space available it will start to truncate the tail, like: Keaton Pickett -> Keaton Pick... -> Keaton... -> etc,
I've been thinking about nesting the cell's content inside a UIScrollView and then scroll it by code when the collectionView scrolls.
I also thought changing the CGRect property of the cell's contents (in this case, the UILabel) while the view is scrolling (playing with the width and minX properties.
However, detecting when the, in this case, UILabel is starting to be out of the screen (like Kea in the first cell of the second picture) is turning to be a struggle.
In my real case scenario I have the UICollectionView (horizontal scroll) nested in UITableView (vertical scroll).
I will thank and appreciate your help in this predicament.
Best regards!
I prefer you to use the code
Objective C
- (void)checkVisibilityOfCell:(MyCustomUITableViewCell *)cell inScrollView:(UIScrollView *)aScrollView {
CGRect cellRect = [aScrollView convertRect:cell.frame toView:aScrollView.superview];
if (CGRectContainsRect(aScrollView.frame, cellRect))
[cell notifyCompletelyVisible];
[cells objectAtIndex:i].hidden= YES; // MAKE THE CELL VISIBLE
else
[cell notifyNotCompletelyVisible];
[cells objectAtIndex:i].hidden= NO; // MAKE THE CELL UNVISIBLE
}
- (void)scrollViewDidScroll:(UIScrollView *)aScrollView {
NSArray* cells = myTableView.visibleCells;
NSUInteger cellCount = [cells count];
if (cellCount == 0)
return;
// Loop through All cells
for (NSUInteger i = 1; i < cellCount - 1; i++)
[[cells objectAtIndex:i] notifyCompletelyVisible];
}

UITableView scroll to the bottom messed up with EstimatedRowHeight

I'm trying to scroll to the bottom of my UITableView (commentsFeed) whenever the user creates a new comment or the user refreshes the UITableView.
The code I use is:
func scrollToBottomOfComments() {
var lastRowNumber = commentsFeed.numberOfRowsInSection(0) - 1
var indexPath = NSIndexPath(forRow: lastRowNumber, inSection: 0)
commentsFeed.scrollToRowAtIndexPath(indexPath, atScrollPosition: .Bottom, animated: true)
}
The problem is here in viewDidLoad:
commentsFeed.rowHeight = UITableViewAutomaticDimension
commentsFeed.estimatedRowHeight = 150
This basically states that the comments can have dynamic heights because users could post either really long comments or really short comments.
When I use the estimatedRowHeight, my scrollToBottom doesn't properly scroll to the bottom because it basically assumes my table height is commentsFeed.count * commentsFeed.estimatedRowHeight
This isn't correct though.
When I remove the estimatedRowHeight though, it doesn't seem to work either, and I think the reason is because it doesn't have the row height calculated properly because the rows each have dynamic heights.
How do I mitigate this?
Edit: It should be stated that the scroll doesn't end up at the right position, but the moment I use my finger to scroll anywhere, then the data jumps into place where it should have been via the scroll
Why don't you calculate the real size of row by something similar to below method.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CustomObject *message = [list.fetchedObjects objectAtIndex:indexPath.row];
//fontChat not available yet
NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:message];
NSRange all = NSMakeRange(0, text.length);
[text addAttribute:NSFontAttributeName value:[UIFont fontWithName:DEFAULT_FONT size:21] range:all];
[text addAttribute:NSForegroundColorAttributeName value:RGB(61, 61, 61) range:all];
CGSize theSize = [text boundingRectWithSize:CGSizeMake(200, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin context:nil].size;
if (theSize.height == 0) {
theSize.height = FIXED_SIZE;
}
return theSize.height;
}
Now for scrolling, I use the following source:
Check it out.
-(void) scrollTolastRow
{
if (self.tableView.contentSize.height > self.tableView.frame.size.height)
{
CGPoint offset = CGPointMake(0, self.tableView.contentSize.height - self.tableView.frame.size.height);
[self.tableView setContentOffset:offset animated:YES];
}
}
There are a couple things that could be affecting your outcome, but it's most likely that your estimated height is not a great estimate. In my experience, anything not particularly close to the true height of the cells will cause havoc on animations. In your case, you mention that the content are comments, or free-form text. I would guess that these cell heights vary wildly, and depending on how your cell is composed, you're probably not going to be able to provide a very accurate estimate, and so you should not provide one. For a large number of cells, this is going to hurt performance, but you probably don't have a choice. Instead, you might want to shift your focus to how you can page in/page out cells into your table to avoid costly calculations, or rearrange your cell to be able to calculate a better estimate. Another suggestion might be to implement estimatedHeightForRowAtIndexPath with an accurate but still less complex algorithm for calculating the height. In any event, a constant value for an estimatedRowHeight will likely never work when you support DynamicType. At the very least, you need to take into account the current DynamicType size.
Other than that, what can you do? Instead of using UITableViewAutomaticDimension, consider implementing heightForRowAtIndexPath and calculating the height of your displayed strings and caching the result (you could use an NSIndexPath -> NSNumber NSCache object). You need to cache the result because without an estimatedHeight, heightForRow is called once for every row when the table is loaded, and then once for every cell as it appears on screen. When using estimatedHeight on iOS 8, estimatedHeight is called once for each cell on launch and heightForRow is called as the cells appear. This is where the estimate is critical, because that is what's used to calculate the contentSize of the UITableView's backing UIScrollView. If the estimated size is wrong, the contentSize is wrong, and so when you ask the tableView to scroll to the last cell, the frame of the last cell is calculated with the bad estimate, which gives you the incorrect contentOffset. Unfortunately, I believe (based on the behavior I see trying to reproduce your question) that when you use UITableViewAutomaticDimension without an estimate, the runtime implicitly estimates an estimate.
EstimatedRowHeight is used by UIKit to estimate whole contentSize (and scrollIndicatorInset), so if you add new row at the end with automatic row dimension, you have to reset estimatedRowHeight to actual average value of whole tableView before you animate scroll.
This is not so good solution because its lot easier to count row height in old style - manually. Or add new cell height value to old table view's content height.
But because you adding new row at the end of table, you can scroll at middle or top position, which ends up with the bottom position, because there is contentInset.bottom = 0. And also the animation will look better.
So:
commentsFeed.scrollToRowAtIndexPath(indexPath, atScrollPosition: .**Middle**, animated: true)
Its look like in .Middle and .Top positions is some condition under the hood in animation which prevent to make a gap between bottom edge and table view's content (+ contentInset.bottom)
P.S. Why not to use the manuall row height calculation?
Because there is autolayout and I believe the "auto" is shortcut for automatic. And also it will save your time and troubles with custom fonts, attributed string, combined cell with more labels and other subviews and so on..
Try this, works for me:
NSMutableArray *visibleCellIndexPaths = [[self.tableView indexPathsForVisibleRows] mutableCopy];
[self.tableView beginUpdates];
[self.tableView reloadRowsAtIndexPaths:visibleCellIndexPaths withRowAnimation:UITableViewRowAnimationNone];
[self.tableView endUpdates];
[self.tableView scrollRectToVisible:CGRectMake(0, self.tableView.contentSize.height - self.tableView.bounds.size.height, self.tableView.bounds.size.width, self.tableView.bounds.size.height) animated:YES];

Why does heightForRowAtIndexPath: come before cellForRowAtIndexPath:?

Almost every time I write an app for a client I have to implement some kind of 'hack' to get UITableViewCells to dynamically become the right height. Depending on the content of the cells this can vary in difficulty.
I usually end up running through code that formats the cell twice, once in heightForRowAtIndexPath: and then again in cellForRowAtIndexPath:. I then use an array or dictionary to store either the height or the formatted cell object.
I've probably written and rewritten this code 20 times over the past 2 years. Why did Apple implement it in this order? It would be much more straightforward to configure the cells and THEN set the height either in cellForRowAtIndexPath: or shortly thereafter in heightForRowAtIndexPath:.
Is there a good reason for the existing order? Is there a better way to handle it?
Best guess: UITableView needs to know the total height of all cells so that it can know the percentage of scroll for the scroll bar and other needs.
Actually, in iOS 7, it doesn't have to work that way. You can now set an estimated height for your rows and calculate each row's real height only when that row is actually needed. (And I assume that this is exactly because people made complaints identical to yours: "Why do I have to do this twice?")
Thus you can postpone the height calculation and then memoize it the first time that row appears (this is for a simple one-section table, but it is easy to adapt it if you have multiple sections):
- (void)viewDidLoad {
[super viewDidLoad];
// create empty "sparse array" of heights, for later
NSMutableArray* heights = [NSMutableArray new];
for (int i = 0; i < self.modeldata.count; i++)
[heights addObject: [NSNull null]];
self.heights = heights;
self.tableView.estimatedRowHeight = 40; // new iOS 7 feature
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
int ix = indexPath.row;
if ([NSNull null] == self.heights[ix]) {
h = // calculate _real_ height here, on demand
self.heights[ix] = #(h);
}
return [self.heights[ix] floatValue];
}
You supplied an estimated height, so all the heights are not asked for beforehand. You are asked for a height only before that row actually appears in the interface, either because it is showing initially or because you or the user scrolled to reveal it.
NOTE Also, note that if you use dequeueReusableCellWithIdentifier:forIndexPath: the cell you get has the correct final height already. That is the whole point of this method (as opposed to the earlier mere dequeueReusableCellWithIdentifier:).

How to layout custom UITableViewCell with varying height

I have a UITableViewCell subclass which has an image, title and description.
I am supposed to resize the cell height according to the description content length i.e. if it spans more than 5 lines, I should extend it (+other subviews like image etc) till it lasts.
The next coming cells should begin only after that.
I have my UITableViewCell subclass instantiated from xib which has a fixed row height = 160.
I know this is pretty standard requirement but I am unable to find any guidelines.
I already extended layoutSubViews like this:
- (void) layoutSubviews
{
[self resizeCellImage];
}
- (void) resizeCellImage
{
CGRect descriptionRect = self.cellDescriptionLabel.frame;
CGRect imageRect = self.cellImageView.frame;
float descriptionBottomEdgeY = descriptionRect.origin.y + descriptionRect.size.height;
float imageBottomEdgeY = imageRect.origin.y + imageRect.size.height;
if (imageBottomEdgeY >= descriptionBottomEdgeY)
return;
//push the bottom of image to the bottom of description
imageBottomEdgeY = descriptionBottomEdgeY;
float newImageHeight = imageBottomEdgeY - imageRect.origin.y;
imageRect.size.height = newImageHeight;
self.cellImageView.frame = imageRect;
CGRect cellFrame = self.frame;
cellFrame.size.height = imageRect.size.height + imageRect.origin.y + 5;
CGRect contentFrame = self.contentView.frame;
contentFrame.size.height = cellFrame.size.height - 1;
self.contentView.frame = contentFrame;
self.frame = cellFrame;
}
It pretty much tells that if description is taller than image, we must resize the image as well as cell height to fit the description.
However when I invoke this code by doing this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
cell.cellDescriptionLabel.text = #"Some long string";
[cell.cellDescriptionLabel sizeToFit];
[cell setNeedsLayout];
return cell;
}
It appears that while cell frame is changed due to layoutSubViews call, other cells do not respect it. That is, they appear on the same position had the previous cell would not have resized itself.
Two questions:
How to make it possible what I want?
Am I doing right by calling setNeedsLayout within cellForRowAtIndexPath?
P.S.: I know heightForRowAtIndexPath holds key to changing the cell height, but I feel that the data parsing (not shown here) that I do as part of cellForRowAtIndexPath would be an overkill just to calculate height. I need something that can directly tell the UITableViewCell to resize itself according to content needs.
-tableView:heightForRowAtIndexPath: is by design how variable sized cells are calculated. The actual frame of a cell is of no importance and is changed by the table view to fit its needs.
You are sort of thinking of this backwards. The delegate tells the table view how cells need to be drawn, then the table view forces cells to fit those characteristics. The only thing you need to provide to the cell is the data it needs to hold.
This is because a table view calculates all the heights of all the cells before it has any cells to draw. This is done to allow a table view to size it's scroll view correctly. It allows for properly sized scroll bars and smooth quick-pans through the table view. Cells are only requested when a table view thinks a cell needs to be displayed to the screen.
UPDATE: How Do I Get Cell Heights
I've had to do this a couple of times. I have my view controller keep a cell which is never used in the table view.
#property (nonatomic) MyTableViewCell *standInCell;
I then use this cell as a stand in when I need measurements. I determine the base height of the cell without the variable sized views.
#property (nonatomic) CGFloat standInCellBaseHeight;
Then in -tableView:heightForRowAtIndexPath:, I get the height for all my variable sized views with the actual data for that index path. I add the variable sized heights to my stand in cell base height. I return that new calculated height.
Note, this is all non-autolayout. I'm sure the approach would be similar, but not identical to this, but I have no experience.
-tableView:heightForRowAtIndexPath: is the preferred way to tell tableview the size of its cells. You may either precalculate and cache it in a dictionary and reuse, or alternatively in ios7, you can use -tableView:estimatedHeightForRowAtIndexPath: to estimate the sizes.
Take a look at this thread - https://stackoverflow.com/questions/18746929/using-auto-layout-in-uitableview-for-dynamic-cell-layouts-variable-row-heights, the answer points to a very good example project here - https://github.com/caoimghgin/TableViewCellWithAutoLayout.
Sorry, but as far as I know you have to implement tableView:heightForRowAtIndexPath:. Warning, in iOS 6 this gets called on every row in you UITableView right away, I think to draw the scrollbar. iOS7 introduces tableView:estimatedHeightForRowAtIndexPath: which if implemented allows you to just guess at the height before doing all the calculation. This can help out a lot on very large tables.
What I found works well is just have your tableView:heightForRowAtIndexPath: call cellForRowAtIndexPath: to get the cell for that row, and then query that cell for it's height cell.bounds.size.height and return that.
This works pretty well for small tables or in iOS7 with tableView:estimatedHeightForRowAtIndexPath implemented.

UITableView content height

I have a UITableView that is set to not enable scrolling, and it exists in a UIScrollView. I'm doing it this way as the design specs call for something that looks like a table view, (actually there are two of them side by side), and it would be much easier to implement tableviews rather than adding a whole bunch of buttons, (grouped table views).
Question is, I need to know how big to make the container view for the scrollview, so it scrolls the whole height of the table views. Once loaded, is there any way to find the height of a tableview? There is no contentView property like a scroll view, frame seems to be static, etc...
Any thoughts?
Use
CGRect lastRowRect= [tableView rectForRowAtIndexPath:index_path_for_your_last_row];
CGFloat contentHeight = lastRowRect.origin.y + lastRowRect.size.height;
You can then use the contentHeight variable to set the contentSize for the scrollView.
A more general solution that works for me:
CGFloat tableViewHeight(UITableView *tableView) {
NSInteger lastSection = tableView.numberOfSections - 1;
while (lastSection >= 0 && [tableView numberOfRowsInSection:lastSection] <= 0)
lastSection--;
if (lastSection < 0)
return 0;
CGRect lastFooterRect = [tableView rectForFooterInSection:lastSection];
return lastFooterRect.origin.y + lastFooterRect.size.height;
}
In addition to Andrei's solution, it accounts for empty sections and section footers.
UITableView is a subclass of UIScrollView, so it has a contentSize property that you should be able to use no problem:
CGFloat tableViewContentHeight = tableView.contentSize.height;
scrollView.contentSize = CGSizeMake(scrollView.contentSize.width, tableViewContentHeight);
However, as several other SO questions have pointed out, when you make an update to a table view (like inserting a row), its contentSize doesn't appear to be updated immediately like it is for most other animated resizing in UIKit. In this case, you may need to resort to something like Michael Manner's answer. (Although I think it makes better sense implemented as a category on UITableView)
You can run over the sections and use the rectForSection to calculate the total height (this included footer and header as well!). In swift I use the following extension on UITableView
extension UITableView {
/**
Calculates the total height of the tableView that is required if you ware to display all the sections, rows, footers, headers...
*/
func contentHeight() -> CGFloat {
var height = CGFloat(0)
for sectionIndex in 0..<numberOfSections {
height += rectForSection(sectionIndex).size.height
}
return height
}
}

Resources