I'm trying to learn auto layout, finally thought I was getting the hang on it when this happened. I'm playing about with prototype cells and labels. I'm trying to have a
titleLbl
- blue box in picturemoneyLbl
- green box in picturesubTitleLbl
- red box in pictureSo far I have this:
What I want to achieve is:
As you can see this is nearly all working, with the exception of the example in the last row. I've tried to research this and from what I can gather it is something to do with the intrinsic content size. Many posts online recommend setting setPreferredMaxLayoutWidth
, i've done this in multiple places for the titleLbl
and it has had no effect. Only when hardcoding it to 180
did I get the results, but it also added whitespace / padding to the other blue boxes on top / underneath the text, greatly increasing the space between the red / blue boxes.
Here are my constraints:
Title:
Money:
Subtitle:
Based on tests i've run with other text samples it does appear to be using the width as shown in the storyboard, but any attempt to correct it isn't working.
I have created an ivar of the cell and used it to create the height of the cell:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
[sizingCellTitleSubMoney.titleLbl setText:[[tableViewData objectAtIndex:indexPath.row] objectForKey:@"title"]];
[sizingCellTitleSubMoney.subtitleLbl setText:[[tableViewData objectAtIndex:indexPath.row] objectForKey:@"subtitle"]];
[sizingCellTitleSubMoney.moneyLbl setText:[[tableViewData objectAtIndex:indexPath.row] objectForKey:@"cost"]];
[sizingCellTitleSubMoney layoutIfNeeded];
CGSize size = [sizingCellTitleSubMoney.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
return size.height+1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
MATitleSubtitleMoneyTableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifierTitleSubTitleMoney];
if(!cell)
{
cell = [[MATitleSubtitleMoneyTableViewCell alloc] init];
}
cell.titleLbl.layer.borderColor = [UIColor blueColor].CGColor;
cell.titleLbl.layer.borderWidth = 1;
cell.subtitleLbl.layer.borderColor = [UIColor redColor].CGColor;
cell.subtitleLbl.layer.borderWidth = 1;
cell.moneyLbl.layer.borderColor = [UIColor greenColor].CGColor;
cell.moneyLbl.layer.borderWidth = 1;
[cell.titleLbl setText:[[tableViewData objectAtIndex:indexPath.row] objectForKey:@"title"]];
[cell.subtitleLbl setText:[[tableViewData objectAtIndex:indexPath.row] objectForKey:@"subtitle"]];
[cell.moneyLbl setText:[[tableViewData objectAtIndex:indexPath.row] objectForKey:@"cost"]];
return cell;
}
I've tried setting setPreferredMaxLayoutWidth
inside the cell's layout subviews:
- (void)layoutSubviews
{
[super layoutSubviews];
NSLog(@"Money lbl: %@", NSStringFromCGRect(self.moneyLbl.frame));
NSLog(@"prefferredMaxSize: %f \n\n", self.moneyLbl.frame.origin.x-8);
[self.titleLbl setPreferredMaxLayoutWidth: self.moneyLbl.frame.origin.x-8];
}
log:
2014-04-30 21:37:32.475 AutoLayoutTestBed[652:60b] Money lbl: {{209, 20}, {91, 61}}
2014-04-30 21:37:32.475 AutoLayoutTestBed[652:60b] prefferredMaxSize: 201.000000
Which is what I would expect as there is 8px between the 2 elements and the console verifies this. It works perfectly on the first row no matter how much text I add to the blue box, which is the same as the storyboard, but any increase to the green box throws off the width calculation. I've tried setting this in the tableView:willDisplayCell
and the tableView:heightForRowAtIndexPath:
as well based on answers from other questions. But no matter what it just doesn't layout.
Any help, ideas or comments would be greatly appreciated
Found an even better answer after reading this: http://johnszumski.com/blog/auto-layout-for-table-view-cells-with-dynamic-heights
creating a UILabel
subclass to override layoutSubviews
like so:
- (void)layoutSubviews
{
[super layoutSubviews];
self.preferredMaxLayoutWidth = CGRectGetWidth(self.bounds);
[super layoutSubviews];
}
This ensures the preferredMaxLayoutWidth
is always correct.
Update:
After several more release's of iOS and testing more usecase's i've found the above to not be sufficient for every case. As of iOS 8 (I believe), under some circumstances only didSet
bounds
was called in time before the screen load.
In iOS 9 very recently I came across another issue when using a modal UIVIewController
with a UITableView
, that both layoutSubviews
and set bounds
were being called after heightForRowAtIndexPath
. After much debugging the only solution was to override set frame
.
The below code now seems to be necessary to ensure it works across all iOS's, controllers, screen sizes etc.
override public func layoutSubviews()
{
super.layoutSubviews()
self.preferredMaxLayoutWidth = self.bounds.width
super.layoutSubviews()
}
override public var bounds: CGRect
{
didSet
{
self.preferredMaxLayoutWidth = self.bounds.width
}
}
override public var frame: CGRect
{
didSet
{
self.preferredMaxLayoutWidth = self.frame.width
}
}
I'm not sure I understand you right and my solution is really far from the best one, but here it is:
I've added a height constraint on label that was cut, connected that constraint to cell's outlet. I've overrided layoutSubview method:
- (void) layoutSubviews {
[super layoutSubviews];
[self.badLabel setNeedsLayout];
[self.badLabel layoutIfNeeded];
self.badLabelHeightConstraint.constant = self.badLabel.intrinsicContentSize.height;
}
and viola! Please try that method and tell me results, thank you.
Omg, just have had the same issue. The @Simon McLoughlin's answer did not help me. But
titleLabel.adjustsFontSizeToFitWidth = true
did a magic! It works, I don't know why but it does. And yes, your label must have an explicit preferredMaxLayoutWidth value either.
I did spend some time with wrapping up the cell, looking like this:
+--------------------------------------------------+
| This is my cell.contentView |
| +------+ +-----------------+ +--------+ +------+ |
| | | | multiline | | title2 | | | |
| | img | +-----------------+ +--------+ | img | |
| | | +-----------------+ +--------+ | | |
| | | | title3 | | title4 | | | |
| +------+ +-----------------+ +--------+ +------+ |
| |
+--------------------------------------------------+
After many attempts I did came with a solution how to use prototype cell that is actually working.
Main trouble was that my labels could be or could be not there. Every element should be optional.
First of all, our 'prototype' cell should only layout stuff when we are not in "real" environment.
Therefore, I did create flag 'heightCalculationInProgress'.
When somebody(tableView's dataSource) is asking to calculate height we are providing data to prototype cell using block: And then we're asking our prototype cell to layout the content and grabbing height out of it. Simple.
In order to avoid iOS7 bug with layoutSubview recursion there is a "layoutingLock" variable
- (CGFloat)heightWithSetupBlock:(nonnull void (^)(__kindof UITableViewCell *_Nonnull cell))block {
block(self);
self.heightCalculationInProgress = YES;
[self setNeedsLayout];
[self layoutIfNeeded];
self.layoutingLock = NO;
self.heightCalculationInProgress = NO;
CGFloat calculatedHeight = [self.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
CGFloat height = roundf(MAX(calculatedHeight, [[self class] defaultHeight]));
return height;
}
This 'block' from the outside could looks like this:
[prototypeCell heightWithSetupBlock:^(__kindof UITableViewCell * _Nonnull cell) {
@strongify(self);
cell.frame = CGRectMake(0, 0, CGRectGetWidth(self.view.frame), 0);
cell.dataObject = dataObject;
cell.multilineLabel.text = @"Long text";
// Include any data here
}];
And now starts the most interesting part. Layouting:
- (void)layoutSubviews {
if(self.layoutingLock){
return;
}
// We don't want to do this layouting things in 'real' cells
if (self.heightCalculationInProgress) {
// Because of:
// Support for constraint-based layout (auto layout)
// If nonzero, this is used when determining -intrinsicContentSize for multiline labels
// We're actually resetting internal constraints here. And then we could reuse this cell with new data to layout it correctly
_multilineLabel.preferredMaxLayoutWidth = 0.f;
_title2Label.preferredMaxLayoutWidth = 0.f;
_title3Label.preferredMaxLayoutWidth = 0.f;
_title4Label.preferredMaxLayoutWidth = 0.f;
}
[super layoutSubviews];
// We don't want to do this layouting things in 'real' cells
if (self.heightCalculationInProgress) {
// Make sure the contentView does a layout pass here so that its subviews have their frames set, which we
// need to use to set the preferredMaxLayoutWidth below.
[self.contentView setNeedsLayout];
[self.contentView layoutIfNeeded];
// Set the preferredMaxLayoutWidth of the mutli-line bodyLabel based on the evaluated width of the label's frame,
// as this will allow the text to wrap correctly, and as a result allow the label to take on the correct height.
_multilineLabel.preferredMaxLayoutWidth = CGRectGetWidth(_multilineLabel.frame);
_title2Label.preferredMaxLayoutWidth = CGRectGetWidth(_title2Label.frame);
_title3Label.preferredMaxLayoutWidth = CGRectGetWidth(title3Label.frame);
_title4Label.preferredMaxLayoutWidth = CGRectGetWidth(_title4Label.frame);
}
if (self.heightCalculationInProgress) {
self.layoutingLock = YES;
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With