iOS自动布局多行UILabel

61
以下问题是这个问题的延续:
iOS:Auto Layout中的多行UILabel
主要思想是每个视图都应该声明它的“首选”(固有)大小,以便AutoLayout知道如何正确显示它。 UILabel只是一个示例,其中一个视图无法自己知道需要什么大小进行显示。 它取决于提供的宽度。
正如mwhuss所指出的那样,setPreferredMaxLayoutWidth使标签跨越多行。 但这不是主要问题。 问题是我在何时何地获取此值,并将其作为参数发送到setPreferredMaxLayoutWidth
我设法做出了看起来合法的东西,请纠正我是否有任何错误,并告诉我是否有更好的方法。
在UIView的
-(CGSize) intrinsicContentSize

我根据self.frame.width为我的UILabel设置了setPreferredMaxLayoutWidth。
UIViewController的
-(void) viewDidLayoutSubviews

这是我知道的第一个回调方法,可以指定主视图中子视图在屏幕上所占用的确切框架。在该方法内部,我会对子视图进行操作,使其固有大小失效,以便根据指定的宽度将UILabel拆分为多行。请注意保留HTML标签。
9个回答

64

objc.io的“多行文本的内在内容大小”一节和高级自动布局工具箱中,有关于这个问题的答案。以下是相关信息:

UILabel和NSTextField的内在内容大小在多行文本中是模糊不清的。文本的高度取决于行的宽度,而这在解决约束时尚未确定。为了解决这个问题,这两个类都有一个称为preferredMaxLayoutWidth的新属性,用于指定计算内在内容大小的最大行宽。

由于我们通常事先不知道这个值,因此我们需要采取两步方法来解决这个问题。首先让自动布局完成其工作,然后使用布局过程中得到的框架来更新首选最大宽度并再次触发布局。

他们提供了在视图控制器中使用的代码:

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    myLabel.preferredMaxLayoutWidth = myLabel.frame.size.width;
    [self.view layoutIfNeeded];
}

看一下他们的帖子,里面有关于为什么需要进行两次布局的更多信息。


5
@AlexanderLongbeach,我不确定您的意思。标签始终位于子视图内部。不可能有不在子视图内的UILabel - nevan king
看到有声称 iOS 7 会为您处理这个问题的说法:http://danielgarbien.com/ios/2013/11/16/preferredMaxLayoutWidth.html - Bob Spryn
1
更新。那篇文章是不正确的。仍需至iOS 8。 - Bob Spryn
在我的情况下,在iOS 8上这个不起作用,因为myLabel.frame.size.width为零。在iOS 7上,“viewDidLayoutSubviews”被调用了三次(首先是零,然后是正确的框架值) - 在iOS 8上只调用一次。现在我使用了‘self.view.bounds.size.width’,现在它正常工作了。 - testing
objc.io中描述的子类化UILabel对我也很有用。现在它适用于iOS 7和iOS 8。 - testing
显示剩余5条评论

44

如果您已经有了明确定义宽度的约束条件,那么 UILabel 不会默认使用它的宽度作为首选的最大布局宽度,这似乎很烦人。

在几乎所有我使用 Autolayout 下的标签的情况下,首选的最大布局宽度一直是标签实际的宽度,一旦进行了其余的布局操作后。

因此,为了自动实现这一点,我使用了一个 UILabel 子类,它重写了 setBounds: 方法。在这里,调用 super 实现,然后(如果还没有这样做)将首选的最大布局宽度设置为边界大小的宽度。

强调这很重要 - 设置首选的最大布局会导致另一个布局过程被执行,因此可能会出现无限循环。


2
在iOS8下,如果UILabel位于tableview单元格中,则不会调用setBounds。有没有办法计算出preferredMaxLayoutWidth的良好初始猜测? - Rog
@RogerNolan 我还没有时间在iOS8上查看它(!) 如果我有机会,我会更新的,或者你可能想问一个新问题。 - jrturton
我刚刚发布了一个解决方案,适用于iOS 7和iOS 8。 - Nick Snyder
2
@RogerNolan iOS8中tableView中UILabel的解决方案有吗? - Pushparaj
1
我也支持iOS 7,所以我将最大布局宽度设置为"Explicit"。现在我想通过编程方式更改iOS 8的最大布局宽度。 - Pushparaj
显示剩余3条评论

33

更新

我的原始答案似乎有所帮助,因此我未进行修改,但是在我的项目中,我发现了一种更可靠的解决方案,可以解决iOS 7和iOS 8中的错误。 https://github.com/nicksnyder/ios-cell-layout

原始答案

这是一个完整的解决方案,对我在iOS 7和iOS 8上都有效。

Objective C

@implementation AutoLabel

- (void)setBounds:(CGRect)bounds {
  if (bounds.size.width != self.bounds.size.width) {
    [self setNeedsUpdateConstraints];
  }
  [super setBounds:bounds];
}

- (void)updateConstraints {
  if (self.preferredMaxLayoutWidth != self.bounds.size.width) {
    self.preferredMaxLayoutWidth = self.bounds.size.width;
  }
  [super updateConstraints];
}

@end

Swift

import Foundation

class EPKAutoLabel: UILabel {

    override var bounds: CGRect {
        didSet {
            if (bounds.size.width != oldValue.size.width) {
                self.setNeedsUpdateConstraints();
            }
        }
    }

    override func updateConstraints() {
        if(self.preferredMaxLayoutWidth != self.bounds.size.width) {
            self.preferredMaxLayoutWidth = self.bounds.size.width
        }
        super.updateConstraints()
    }
}

这对我有用。我采用了这个逻辑,因为我只需要在label.numberOfLines = 0;时去除Xcode警告。 - Asif Bilal
这个在 iOS 8 中在 UITableViewCell 中对我有效。 - nsantorello
这也是唯一对我在UITableViewCell中起作用的建议。非常感谢! - yura
这在我的iOS 7上不起作用!我添加了以下内容:
  • (void)layoutSubviews { [super layoutSubviews]; self.preferredMaxLayoutWidth = CGRectGetWidth(self.bounds); [super layoutSubviews];
}
- Rudolf J
在集合和表格中都能完美运行,通过不像其他解决方案那样添加大量标签边界的重新计算,使列表和集合变得流畅。 - Softlion
1
我认为Swift版本永远不会触发self.setNeedsUpdateConstraints()这一行。它应该检查bounds.size.width != oldValue.size.width是否成立。目前的写法是将值与自身进行比较。 - aranasaurus

10

我们遇到这样一种情况:一个在UIScrollView中自动布局的UILabel在纵向布局时布局良好,但旋转到横向后,UILabel的高度未被重新计算。

我们发现@jrturton的回答解决了这个问题,可能是因为现在正确设置了preferredMaxLayoutWidth。

以下是我们使用的代码。只需将Interface Builder中的Custom类设置为CVFixedWidthMultiLineLabel.

CVFixedWidthMultiLineLabel.h

@interface CVFixedWidthMultiLineLabel : UILabel

@end 

CVFixedWidthMultiLineLabel.m

@implementation CVFixedWidthMultiLineLabel

// Fix for layout failure for multi-line text from
// https://dev59.com/QmMm5IYBdhLWcg3wm_7z
- (void) setBounds:(CGRect)bounds {
    [super setBounds:bounds];

    if (bounds.size.width != self.preferredMaxLayoutWidth) {
        self.preferredMaxLayoutWidth = self.bounds.size.width;
    }
}

@end

完美运行!谢谢。 - Orion Edwards
1
我也遇到了同样的问题,但不幸的是这个方法对我并没有起作用。我正在处理tableView中的标签。 - Eichhörnchen
1
这个解决方案还是挺好的,但有一个小问题。我在tableView中使用了label,在heightForRowAtIndexpath方法中我通常会使用'systemLayoutSizeFittingSize'来获取正确的单元格尺寸。然而,在调用此方法之前,您必须将单元格宽度更新为'dequeueReusableCellWithIdentifier'将以xib文件中定义的大小返回它(我的大小为320,实际大小为227)。因此,在调用layoutIfNeeded之前,更新cell frame = tableView.contentSize.width,结合这个解决方案可以在所有设备上(4s、5、6、6+)运行。 - hris.to
我在UICollectionViewCell中唯一有效的方法是在计算大小时设置初始框架。 - D6mi

2
由于我无法添加评论,所以我被迫将它作为答案添加。 对于jrturton的版本,只有在我在updateViewConstraints中调用layoutIfNeeded之后才会起作用,然后才获取所讨论标签的preferredMaxLayoutWidth。 如果不调用layoutIfNeeded,则在updateViewConstraints中preferredMaxLayoutWidth始终为0。 然而,在setBounds:中检查时,它始终具有所需的值。 我没能知道何时设置了正确的preferredMaxLayoutWidth。 我重写了UILabel子类上的setPreferredMaxLayoutWidth:,但从未被调用过。
总结一下,我:
  • ...继承了UILabel
  • ...并覆盖了setBounds:方法,如果尚未设置,则将preferredMaxLayoutWidth设置为CGRectGetWidth(bounds)
  • ...在以下操作之前调用[super updateViewConstraints]
  • ...在获取用于标签大小计算的preferredMaxLayoutWidth之前调用layoutIfNeeded

编辑:这个解决方法似乎只有在某些情况下才有效或需要。我遇到了一个问题(iOS 7/8),即在布局过程执行一次后,preferredMaxLayoutWidth返回0时,标签的高度没有正确计算。因此,在经过一番试验和错误后(并找到这篇博客文章),我又切换回使用UILabel,只是设置了上、下、左、右的自动布局约束。出于某种原因,更新文本后标签的高度被正确设置。


2

使用boundingRectWithSize

我通过使用boundingRectWithSize来解决了在一个遗留的UITableViewCell中使用"\n"作为换行符时两个多行标签的问题,示例如下:

- (CGFloat)preferredMaxLayoutWidthForLabel:(UILabel *)label
{
    CGFloat preferredMaxLayoutWidth = 0.0f;
    NSString *text = label.text;
    UIFont *font = label.font;
    if (font != nil) {
        NSMutableParagraphStyle *mutableParagraphStyle = [[NSMutableParagraphStyle alloc] init];
        mutableParagraphStyle.lineBreakMode = NSLineBreakByWordWrapping;

        NSDictionary *attributes = @{NSFontAttributeName: font,
                                     NSParagraphStyleAttributeName: [mutableParagraphStyle copy]};
        CGRect boundingRect = [text boundingRectWithSize:CGSizeZero options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil];
        preferredMaxLayoutWidth = ceilf(boundingRect.size.width);

        NSLog(@"Preferred max layout width for %@ is %0.0f", text, preferredMaxLayoutWidth);
    }


    return preferredMaxLayoutWidth;
}

那么调用该方法就变得非常简单了:

CGFloat labelPreferredWidth = [self preferredMaxLayoutWidthForLabel:textLabel];
if (labelPreferredWidth > 0.0f) {
    textLabel.preferredMaxLayoutWidth = labelPreferredWidth;
}
[textLabel layoutIfNeeded];

我可以使用相同的方法来计算标签高度吗? - Siddharthan Asokan

1

根据另一个答案的建议,我尝试覆盖viewDidLayoutSubviews

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    _subtitleLabel.preferredMaxLayoutWidth = self.view.bounds.size.width - 40;
    [self.view layoutIfNeeded];
}

这样做可以让代码正常运行,但会在用户界面上出现“可见闪烁”的问题,即首先以两行的高度呈现标签,然后再以只有一行高度重新呈现标签。这对我来说是不可接受的。后来,我发现通过重写updateViewConstraints方法,可以找到更好的解决方案:
-(void)updateViewConstraints {
    [super updateViewConstraints];

    // Multiline-Labels and Autolayout do not work well together:
    // In landscape mode the width is still "portrait" when the label determines the count of lines
    // Therefore the preferredMaxLayoutWidth must be set
    _subtitleLabel.preferredMaxLayoutWidth = self.view.bounds.size.width - 40;
}

这对我来说是更好的解决方案,因为它不会导致“视觉闪烁”。

0

0
一个简洁的解决方案是将 rowcount = 0 设置为零,并使用标签高度约束的属性。然后在内容设置完成后调用。
CGSize sizeThatFitsLabel = [_subtitleLabel sizeThatFits:CGSizeMake(_subtitleLabel.frame.size.width, MAXFLOAT)];
_subtitleLabelHeightConstraint.constant = ceilf(sizeThatFitsLabel.height);

-(void) updateViewConstraints自iOS 7.1以来存在问题。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接