I need to force UITableView to load all cells based on datasource and get resulting UITableView height before it will be displayed on UI. I have other logic to be implemented based on that height. My rows have dynamic height and content inside cells is autolayouted. That is why I coudln't calculate it manually (or don't know how). It is crucial in my case to get the height before UITableView is displayed.
Please advise.
Now, the requirement is to find the height of cells with filled data
By this, you can find height of cells before displaying them on tableView
Yes, you can do that by using below code:
/****/
- (CGFloat)heightForBasicCellAtIndexPath:(NSIndexPath *)indexPath
{
NSString *cellIdentifier= #"IDENTIFIER";
static dispatch_once_t onceToken;
static UITableViewCell *cell = nil;
dispatch_once(&onceToken, ^{
cell = [self.tableView dequeueReusableCellWithIdentifier:cellIdentifier];
});
/**SET ALL DATA AND VALUES ON CELL*/
CGFloat height = [self calculateHeightForConfiguredSizingCell:cell];
// NSLog(#"height at indexPath %ld --> %f",(long)indexPath.row, height);
return height;
}
- (CGFloat)calculateHeightForConfiguredSizingCell:(UITableViewCell *)sizingCell {
[sizingCell setNeedsLayout];
[sizingCell layoutIfNeeded];
CGSize size = [sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
CGFloat height = size.height;
return height + 1.0f; // Add 1.0f for the cell separator height
}
Related
1.When the content inside the cell is too long, cell expansion, Or then shrink, UITableView will scroll to the back of the cell position.
2.I want cell to roll back to where it started expansion.
my code:
((PartnershipsTableViewCell *)cell).commentSpreadButtonClickHandler = ^() {
// just call - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
[weakSelf.tableView beginUpdates];
[weakCell configUI];
[weakSelf.tableView endUpdates];
// if here use "[weakSelf.tableView reloadData]",
// it can be correct,
// Unless on the first cell which have the expansion button.
};
then update uitableview cell's height. but the result isn't what i want
- (void)configUI {
if ([self.baseModel isKindOfClass: [UserWorldDynamicModel class]]) {
self.model = (id)self.baseModel;
}
[self setupValue];
}
- (void)setupValue {
// setup the property value, and update the constraints with masonry
}
// the button : read less or read more
- (void)setSpreadButton {
NSString *text = self.model.isContentOpen ? #"read less" : #"read more";
if (!self.spreadButton) {
self.spreadButton = [MYSUtil createButtonWithTitle: text target: self sel: #selector(spreadButtonClick:) image: nil font: Font14 color: DarkBlueTextColor cornerRadius: 0];
self.spreadButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
}
if (self.model.shouldShowSpreadButton) {
if (!self.spreadButton.superview) {
[self.whiteBackgroudView addSubview: self.spreadButton];
}
[self.spreadButton setTitle: text forState: UIControlStateNormal];
self.spreadButton.selected = [self.model.isSpreadState intValue];
self.spreadButton.frame = CGRectMake(CGRectGetMinX(self.contentLabel.frame), CGRectGetMaxY(self.contentLabel.frame), 80, 30);
self.tempView = self.spreadButton;
} else {
if (self.spreadButton.superview) {
[self.spreadButton removeFromSuperview];
}
}
}
// calculate the height of the label and compare it with the fixed value
NSMutableAttributedString *string = [self createMutableAttibuteStringWithNSString: text withFont: font];
self.contentLabel.attributedText = string;
// here calculate maxContentLabelHeight with
CGFloat maxContentLabelHeight = self.contentLabel.font.pointSize * (numberOfLines + 1) + 16;
// here calculate the NSMutableAttributedString's height
YYTextContainer *container = [YYTextContainer containerWithSize:CGSizeMake(width, MAXFLOAT)];
YYTextLayout *textLayout = [YYTextLayout layoutWithContainer:container text: string];
CGSize size = textLayout.textBoundingSize;
CGFloat height = size.height;
// then compare the NSMutableAttributedString's height with the fixed value. if true, show the spreadButton
if (height > maxContentLabelHeight) {
self.model.shouldShowSpreadButton = YES;
// storage the real height and temp height, use to calculate the tableView's contentOffset, when cell from expansion state to shrinking state in block.
self.model.contentHeight = height;
self.model.tempContentHeight = maxContentLabelHeight;
}
// if height > maxContentLabelHeight and the property "isContentOpen" of the viewModel, the height value is maxContentLabelHeight, Or not, the height value is height
if (!self.model.isContentOpen && height > maxContentLabelHeight) {
height = maxContentLabelHeight;
}
// no matter cell is expansion state or shrinking state, reset label's frame.
self.contentLabel.frame = CGRectMake(x, CGRectGetMaxY(self.headerImageView.frame) + Margin_Top, width, height);
readMore/ readLess block
before tableView reloadData on mainQueue, record it's contentOffset, Used to calculate the position of the tableView need to scroll. like this:
CGPoint point = weakSelf.tableView.contentOffset;
reloadData : refresh tableView On mainQueue.
when reloadData complete, scroll tableView to the position which expanded. when tableView from expansion state to shrinking state, and the height of expansion state is greater than 70% of the Screen's height, scroll the tableView (70% is ma condition, you can change is according to your condition)
- (UITableViewCell *)tableView:(UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath {
// here is your code
PartnershipsTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: cellIdentifierString];
[cell config];
((PartnershipsTableViewCell *)cell).spreadButtonClickHandler = ^() {
CGPoint point = weakSelf.tb.contentOffset;
[weakSelf.tb reloadData];
if (!baseModel.isContentOpen && baseModel.contentHeight > SCREEN_HEIGHT * 0.7) {
point.y -= baseModel.contentHeight - baseModel.tempContentHeight;
dispatch_async(dispatch_get_main_queue(), ^{
weakSelf.tb.contentOffset = point;
});
}
};
return cell;
}
- (CGFloat)tableView:(UITableView *) tableView heightForRowAtIndexPath:(NSIndexPath *) indexPath {
return 70;
}
in my issue, I find a another interesting problem, when use the follow method in your code. if your cell have great changes, especially the height of the cell. when you use the method [tableView reloadData], you'd better not use the follow method.
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 70;
}
then use the method [tableView reloadData] to refresh UI, tableView will scroll to any position, so I delete it. because, this method use to estimated height of cell, then use to estimate tableView's contentSize, if the height between estimated and actual is bigger difference, use the method [tableView reloadData], will cause the tableView scroll to anywhere.(don't ask me how to know, this is a painful process for me).
I have solved the problem, leave some notes to myself, and for every one, and hope my solution can help you too.
thanks for #pckill and #Theorist, without your suggestion, I can't solve my question so perfect, thank you very much.
#Theorist I have reedited my code in my mind. Perhaps, the readability is better now.
I have a custom UITableViewCell that has a dynamic image height. All the resizing works as its supposed to when the cell is created. The issue is once the cell is reused by the UITableView the constraint for the image height is changed and causes an “Unable to simultaneously satisfy constraints” error.
I have tried deactivating the height constraint before resetting it but it still causes the error.
With regards to setting the height anchor. This creates a new instance each time, are the deactivated constraints released from memory? Is there a way of just updating the current constraint in this format?
Should i be doing something within - (void)prepareForReuse method?
Thanks for any help
// Within Custom UITableViewCell
// The cell is a prototype cell in StoryBoard
// The width of the image is fixed and already set
// Observe when the image is set for imgView
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
if ([keyPath isEqualToString:#"image"]) {
if(self.imgView.image) {
CGFloat ratio = self.imgView.image.size.height / self.imgView.image.size.width;
self.imageHeightConstraint.active = NO;
self.imageHeightConstraint = [self.imgView.heightAnchor constraintEqualToAnchor:self.imgView.widthAnchor multiplier:ratio];
self.imageHeightConstraint.identifier = #"imageHeightConstraint";
self.imageHeightConstraint.active = YES;
}
}
}
The UITableViewCell auto resizes as the UIImageView is in a UIStackView which has its top and bottom anchors fixed to the top and bottom of the cells contentView.
- (void)buildUI {
UIStackView *containerStackView = [UIStackView new];
[self.contentView addSubview:containerStackView];
containerStackView.translatesAutoresizingMaskIntoConstraints = NO;
containerStackView.axis = UILayoutConstraintAxisHorizontal;
containerStackView.distribution = UIStackViewDistributionFill;
containerStackView.alignment = UIStackViewAlignmentCenter;
containerStackView.spacing = 10;
[containerStackView.topAnchor constraintEqualToAnchor:self.contentView.topAnchor constant:10.0].active = YES;
[containerStackView.centerXAnchor constraintEqualToAnchor:self.contentView.centerXAnchor].active = YES;
[containerStackView.widthAnchor constraintEqualToAnchor:self.contentView.widthAnchor multiplier:kImageWidthMultiplier].active = YES;
[containerStackView.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor constant:-30.0].active = YES;
self.imgView = [UIImageView new];
[containerStackView addArrangedSubview:self.imgView];
self.imgView.translatesAutoresizingMaskIntoConstraints = NO;
[self.imgView.widthAnchor constraintEqualToAnchor:containerStackView.widthAnchor multiplier:0.492].active = YES;
}
It is quite complicated for calculate dynamic table view cell height. In you case, cell height depend on image height.
When table view cell is reused, all cell's properties will be reused and the height is not exception. The easy way to make change is using heightForRowAtIndexPath, but it very hard when using autolayout to calculate height.
According this article (http://www.raywenderlich.com/87975). The subview in table view cell will be compressed and after that, we could easy to get height and return in heightForRowAtIndexPath. However, it take a little time to calculate and make your scroll is not smooth, but we could using estimatedRowHeight, your tableview will use the estimate height and apply for specific row before we can calculated exactly height for this row
The below is short demo code:
//Estimate height (height could be 35, 42, 50, 29, etc)
//Just estimate a number
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == 0) {
return 40.0f;
}
//Your exactly height for row
- (CGFloat)calculateHeightForConfiguredSizingCell:(UITableViewCell *)sizingCell {
[yourCustomCell setNeedsLayout];
[yourCustomCell layoutIfNeeded];
CGSize size = [yourCustomCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
return size.height;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
//only set once
static YourCustomTableViewCell *yourCustomCell = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//load custom cell
yourCustomCell = (YourCustomTableViewCell*)[Utils getCellNibFile:#"YourCustomTableViewCell" Owner:self];
});
//get image from datasource
UIImage* image = [self.dataSource objectAtIndex:indexPath];
//set image for cell and begin calculate height after set image
[yourCustomCell setImage:image];
[self calculateHeightForConfiguredSizingCell:yourCustomCell];
return height;
}
I am creating an application with iOS 7 devices. This application contains UITableView with dynamic cell height.
To create this I have a custom cell which contains methods:
- (CGFloat)heightWithModel:(Model *)model
{
CGFloat height = 0.0f;
height = self.contentLabel.frame.origin.y + [self contentHeightWithModel:model] + CellBottomOffset;
return height;
}
and...
- (CGFloat)contentHeightWithModel:(Model *)model
{
CGFloat height = 0.0;
NSDictionary *attributes = [NSDictionary dictionaryWithObject:[UIFont systemFontOfSize:14.0f] forKey:NSFontAttributeName];
NSString *string = model.content;
NSStringDrawingContext *context = nil;
NSStringDrawingOptions options = NSStringDrawingUsesLineFragmentOrigin;
CGSize size = CGSizeMake(self.contentLabel.bounds.size.width, CGFLOAT_MAX);
CGRect frame = [string boundingRectWithSize:size options:options attributes:attributes context:context];
height = frame.size.height;
return height;
}
In my view controller, I have implemented UITableViewDelegate protocol method:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
CGFloat height;
static Cell *cell;
if (!cell)
{
cell = [tableView dequeueReusableCellWithIdentifier:CellReuseIdentifier];
}
height = [cell heightWithModel:[self.dataSource.models objectAtIndex:indexPath.row]];
return height;
}
As far as I understand, this should be enough to create a table view with dynamic cell height. Despite this, I have a table view like this:
As you see, part of text are hidden, because label (red one) height is too small. Cell height is set dynamically by using Auto Layout (10 px from the bottom).
Can anyone see where is the problem?
First take a variable
CGFloat height;
put this lines in viewdidload method
in that set estimated height of cell and for that label set lines to 0 in inspector panel and change its height = to >= and give constraint from all side
[self.tbl_rating layoutIfNeeded];
[self.tbl_rating setNeedsLayout];
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_7_1 )
{
self.tbl_rating.estimatedRowHeight=153.0f;
self.tbl_rating.rowHeight=UITableViewAutomaticDimension;
}
Now put this two method for finding label height and dynamic cell height increment
In this method set your label text that you retriving from like array or web service
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// 7.1>
if (NSFoundationVersionNumber <= NSFoundationVersionNumber_iOS_7_1 ) {
height=[self findHeightForText:[NSString stringWithFormat:#"%#",[[arr_review valueForKey:#"desc"]objectAtIndex:indexPath.row]] havingWidth:self.view.frame.size.width andFont:[UIFont systemFontOfSize:12.0f]].height;
return 153+height;
}
return UITableViewAutomaticDimension;
}
-(CGSize)findHeightForText:(NSString *)text havingWidth:(CGFloat)widthValue andFont:(UIFont *)font {
CGSize size = CGSizeZero;
if (text) {
CGRect frame = [text boundingRectWithSize:CGSizeMake(widthValue, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:#{ NSFontAttributeName:font } context:nil];
size = CGSizeMake(frame.size.width, ceil(frame.size.height));
}
return size;
}
NOTE: In tableview method heightForRowAtIndexPath I recommand you pass text from array value. If this thing doesnt work then check your constraints.
This thing works for me Hope it will work.
Thank you.
I've got some strange problem related to dynamically sized cells, auto layout and size classes. My test project is completely based on Ray's tutorial. It seems that the only difference is UILabels's font sizes for size classes.
The height calculation is wrong when I set another font size for label in some size class. I've made screenshot to illustrate it.
Wwith wrong cell's height calculations:
With correct cell's height calculations:
In addition, I've pushed test project to the github.
EDITED:
ViewController.m
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self heightForCellAtIndexPath:indexPath];
}
- (CGFloat)heightForCellAtIndexPath:(NSIndexPath *)indexPath {
static CustomTableViewCell *sizingCell = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sizingCell = [self.tableView dequeueReusableCellWithIdentifier:kBasicCell];
});
[self configurateCell:sizingCell atIndexPath:indexPath];
return [self calculateHeightForCell:sizingCell];
}
- (CGFloat)calculateHeightForCell:(CustomTableViewCell *)cell {
cell.bounds = CGRectMake(0.0f, 0.0f, CGRectGetWidth(self.tableView.frame), CGRectGetHeight(cell.bounds));
[cell setNeedsLayout];
[cell layoutIfNeeded];
CGSize size = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
return size.height +1.0f;
}
CustomLabel.m
- (void)setBounds:(CGRect)bounds {
[super setBounds:bounds];
if (self.numberOfLines == 0 && bounds.size.width != self.preferredMaxLayoutWidth) {
self.preferredMaxLayoutWidth = self.bounds.size.width;
[self setNeedsUpdateConstraints];
}
Since you're already using Auto Layout, I'd recommend also taking advantage of self-sizing cells. You won't need any row height calculation, as iOS can adjust the cell's height automatically, based on the UILabel height.
Add Auto Layout constraints to your contentView's UILabel. This will cause the cell to adjust its contentView height based on the label's contents.
Enable row height estimation.
self.tableView.rowHeight = UITableViewAutomaticDimension;
self.tableView.estimatedRowHeight = 44.0;
For more information, see the detailed walkthrough by smileyborg in his answer to Using Auto Layout in UITableView for dynamic cell layouts & variable row heights.
This is my story:
I have a problem with the size of my IUTableViewCell. When I add several cell, the cell auto resizing.
any answer will be appreciated :)
That my code to resize:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPathInCellTable:(NSIndexPath *)indexPath {
CustomCell *cell = (CustomCell*) [self tableView:tableView
cellForRowAtIndexPath:indexPath];
CGSize size;
// SIZE HEIGHT TEXT
size = [cell.color.text sizeWithAttributes:
#{NSFontAttributeName:
[UIFont systemFontOfSize:12.0f]}];
// SIZE HEIGHT FOR CELL
CGRect frame = [cell frame];
frame.size.height += size.height;
[cell setFrame:frame];
// SIZE HEIGHT IMG
CGRect frame = [cell.img frame];
frame.size.height = 69;
frame.size.width = 69;
[cell.img setFrame:frame];
if (indexPath.row == 0) [self setHeightTableView:0];
_tableHeightConstraint.constant += cell.frame.size.height;
return cell.frame.size.height;
}
There some screenshot :
the first time i add a cell everything is fine
the same for the second cell everything is fine
And there the problem comes
You call CustomCell
*cell = (CustomCell*) [self tableView:tableView
cellForRowAtIndexPath:indexPath];
to get the a cell, which is wrong, because there is no cell created yet (so its nil). tableView:heightForCell:atIndexPath: get called before the cell was created. The best solution would be to have a module abject to save the height needed for your cell or make some similar calculations
Use a prototype cell to get the sizing instead of using tableView:cellForRowAtIndexPath:
http://www.samrayner.com/posts/dynamic-tableview-cells/