Custom UITableViewCell constraint issue on cell reuse - ios

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;
}

Related

The UITableViewCell's expansion with long long content in iOS

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.

Force UITableView to load cells before it is displayed

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
}

Unexpected behavior of systemLayoutSizeFittingSize: and size classes

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.

UILabel's inside UITableViewCell not resizing when orientation changes

I'm populating my UITableView with cells that contain two UILabel's: one of them being the title and the other one being the content. I'm calculating their sizes in heightForRowAtIndexPath: and everything seems cool until I change the orientation. When I change the orientation, the size of each label doesn't seem to change. For example if I'm changing from portrait to landscape, the expected behaviour is to see label's with smaller height (assuming it's a multi-line label). However, the labels' heights remain the same and I get an ugly, disoriented view. The code I've written for this is as follows:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
CGFloat tableWidth = [tableView bounds].size.width;
ListItem* glossaryItem;
if ([tableView isEqual:self.searchDisplayController.searchResultsTableView])
{
glossaryItem =(ListItem*)[self.searchResults objectAtIndex:indexPath.row];
}
else
{
glossaryItem =(ListItem*)[self.glossary objectAtIndex:indexPath.row];
}
NSString* name = [NSString stringWithFormat:#"<p><b>%#</b> :</p>", glossaryItem.name];
NSString* content = glossaryItem.details[0];
NSAttributedString* nameWithStyle = [HTMLParser parseText:name withFontSize:16 withFontType:#"Light"];
NSAttributedString* contentWithStyle = [HTMLParser parseText:content withFontSize:16 withFontType:#"Light"];
CGRect nameRect = [nameWithStyle boundingRectWithSize:CGSizeMake(screenWidth - 40, CGFLOAT_MAX) options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading) context:NULL];
CGRect contentRect = [contentWithStyle boundingRectWithSize:CGSizeMake(screenWidth - 40, CGFLOAT_MAX) options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading) context:NULL];
return nameRect.size.height + contentRect.size.height + 33;
}
HTMLParser is a class I wrote to parse text's in HTML format into attributed strings. I'm guessing the problem here is the label's sizes are not recalculated after orientation change. If I add the following code, everything looks fine but this takes too much time for tables with a lot of cells.
- (void) didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation{
[self.tableView reloadData];
}
How can I work around this? I can provide the constraints of my labels if needed.
Setup autoLayout constraints
or, continue to call the reloadData the way you are doing on didRotate...
but size the labels in cellForRow... like below
Set the height for row only in heightForRowAtIndexPath
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
//Some identifier and recycling stuff
if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)) {
//Make labels smaller
}
else {
//Make them longer
}
// save the calculated height for the label
//and size your tableviewrow height accordingly in heightForRowAtIndexPath
}

Resize tableview NSLayoutConstraint

Hi I have a tableView which I change the width programmatically by his constraint:
self.widthTableLeft.constant = self.view.frame.size.width;
I do this in the viewDidLoad.
In the delegate method:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
I need the contentview frame of each cell to calculate the hight of the cell, but when get it, this is the old size, I mean the size if I wouldn't have done the resize.
When the table is showed the size of the table is right but the hight of the cell is wrong.
I have tried to call:
[cell setNeedsUpdateConstraints];
[cell updateConstraintsIfNeeded];
[cell.contentView setNeedsLayout];
[cell.contentView layoutIfNeeded];
The question is, before to show the view of my view controller, when is the best place to update the width constraint of a UITableView and to get the right contentView in the cells in the method heightForRowAtIndexPath.
Sorry but I'm lost, could you give me any ideas?
Thanksssss
UPDATE
This is the code in heightForRowAtIndexPath
static const NSInteger ROC_TITLE_LABEL_TAG = 2;
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
ROCAgendaItemDetailEntity *agendaItemDetail = [self.listRight objectAtIndex:indexPath.row];
UITableViewCell *cell = [self.tableLeft dequeueReusableCellWithIdentifier:#"cellVideo"];
//I get the label which will make the cell bigger
UILabel *titleLabel = (UILabel *)[cell viewWithTag:ROC_TITLE_LABEL_TAG];
//We init the height title with the min height.
float extraSpaceHeightLabel = 0;
if (agendaItemDetail.agendaItem.title.length > 0) {
//I will use his width to get the hight that I need to write all text
float width = titleLabel.frame.size.width;
CGSize size = CGSizeMake(width, CGFLOAT_MAX);
//we get the size
CGSize sizeTitleLabel = [agendaItemDetail.agendaItem.title sizeWithFont:[ROCTextsInformation imagoMed:15] constrainedToSize:size lineBreakMode:titleLabel.lineBreakMode];
//we substract the current height of the label to know the extra space to write all text
sizeTitleLabel.height -= titleLabel.frame.size.height;
extraSpaceHeightLabel = sizeTitleLabel.height;
}
//The final hight will be the contenViewHeight pluss the extra space needed.
return cell.contentView.frame.size.height + extraSpaceHeightLabel;
}
and this is the cell in the table view
Thanks again
viewDidLoad is too early to try and access the geometries of your views. Instead, access self.view's width in viewDidLayoutSubviews.
#interface CustomViewController ()
#property (nonatomic) BOOL isFirstTimeViewDidLayoutSubviews; // variable name could be re-factored
#end
#implementation CustomViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.isFirstTimeViewDidLayoutSubviews = YES;
}
- (void)viewDidLayoutSubviews
{
// only after layoutSubviews executes for subviews, do constraints and frames agree (WWDC 2012 video "Best Practices for Mastering Auto Layout")
if (self.isFirstTimeViewDidLayoutSubviews) {
// execute geometry-related code...
self.widthTableLeft.constant = self.view.frame.size.width;
}
self.isFirstTimeViewDidLayoutSubviews = NO;
}

Resources