如何使用自动布局调整父视图大小以适应所有子视图?

148

我对自动布局的理解是它基于约束和内在大小,计算子视图的位置,并考虑父视图的大小。

是否有一种方法可以反向实现这个过程?我想根据约束和内在大小来调整父视图的大小。最简单的实现方式是什么?

我在Xcode中设计了一个视图,用作UITableView的标题。这个视图包含一个标签和一个按钮。标签的大小取决于数据。根据约束,标签成功地将按钮向下推动,或者如果存在按钮与父视图底部之间的约束,则标签被压缩。

我找到了一些类似的问题,但它们没有好的且易于操作的答案。


28
您应该从以下答案中选择一个,因为Tom Swift肯定回答了您的问题。所有海报都花费了大量时间来帮助您,现在您应该尽自己的一份力并选择您最喜欢的答案。 - David H
4个回答

153

正确的API是UIView systemLayoutSizeFittingSize:,传递UILayoutFittingCompressedSizeUILayoutFittingExpandedSize参数。

对于使用autolayout的普通UIView,只要您的约束条件正确,这应该可以正常工作。如果您想在UITableViewCell上使用它(例如确定行高),则应针对单元格的contentView调用它并获取高度。

如果您的视图中有一个或多个多行UILabel,则需要考虑更多因素。对于这些,必须正确设置preferredMaxLayoutWidth属性,以便标签提供正确的intrinsicContentSize,该尺寸将用于systemLayoutSizeFittingSize的计算。

编辑:根据请求,添加一个表格视图单元格高度计算的示例

对于表格单元格高度计算来说,使用autolayout不是非常高效,但确实非常方便,特别是如果您有一个具有复杂布局的单元格。

如上所述,如果使用多行UILabel,则将preferredMaxLayoutWidth与标签宽度同步非常重要。我使用自定义的UILabel子类来实现这一点:

@implementation TSLabel

- (void) layoutSubviews
{
    [super layoutSubviews];

    if ( self.numberOfLines == 0 )
    {
        if ( self.preferredMaxLayoutWidth != self.frame.size.width )
        {
            self.preferredMaxLayoutWidth = self.frame.size.width;
            [self setNeedsUpdateConstraints];
        }
    }
}

- (CGSize) intrinsicContentSize
{
    CGSize s = [super intrinsicContentSize];

    if ( self.numberOfLines == 0 )
    {
        // found out that sometimes intrinsicContentSize is 1pt too short!
        s.height += 1;
    }

    return s;
}

@end

这是一个编造的UITableViewController子类,演示了heightForRowAtIndexPath:

#import "TSTableViewController.h"
#import "TSTableViewCell.h"

@implementation TSTableViewController

- (NSString*) cellText
{
    return @"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.";
}

#pragma mark - Table view data source

- (NSInteger) numberOfSectionsInTableView: (UITableView *) tableView
{
    return 1;
}

- (NSInteger) tableView: (UITableView *)tableView numberOfRowsInSection: (NSInteger) section
{
    return 1;
}

- (CGFloat) tableView: (UITableView *) tableView heightForRowAtIndexPath: (NSIndexPath *) indexPath
{
    static TSTableViewCell *sizingCell;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        sizingCell = (TSTableViewCell*)[tableView dequeueReusableCellWithIdentifier: @"TSTableViewCell"];
    });

    // configure the cell
    sizingCell.text = self.cellText;

    // force layout
    [sizingCell setNeedsLayout];
    [sizingCell layoutIfNeeded];

    // get the fitting size
    CGSize s = [sizingCell.contentView systemLayoutSizeFittingSize: UILayoutFittingCompressedSize];
    NSLog( @"fittingSize: %@", NSStringFromCGSize( s ));

    return s.height;
}

- (UITableViewCell *) tableView: (UITableView *) tableView cellForRowAtIndexPath: (NSIndexPath *) indexPath
{
    TSTableViewCell *cell = (TSTableViewCell*)[tableView dequeueReusableCellWithIdentifier: @"TSTableViewCell" ];

    cell.text = self.cellText;

    return cell;
}

@end

一个简单的自定义单元格:

#import "TSTableViewCell.h"
#import "TSLabel.h"

@implementation TSTableViewCell
{
    IBOutlet TSLabel* _label;
}

- (void) setText: (NSString *) text
{
    _label.text = text;
}

@end

这是Storyboard中定义的约束条件的图片。请注意,标签上没有高度/宽度的约束-这些约束从标签的intrinsicContentSize推断出来:

enter image description here


1
你能举个例子,展示如何使用heightForRowAtIndexPath:方法来处理包含多行标签的单元格吗?我已经尝试过很多次,但都没有成功。如果单元格是在Storyboard中设置的,该怎么做才能获取单元格呢?需要哪些约束条件才能使其正常工作? - rdelmar
7
除非我从单元格底部添加一个垂直约束,连接到单元格中最低的子视图底部,否则此方法对我无效。似乎垂直约束必须包括单元格和其内容之间的顶部和底部垂直间距,才能成功计算单元格高度。 - Eric Baker
谢谢您,先生! 我在我的项目中使用它。 https://github.com/kuchumovn/sociopathy.ios/blob/master/Sociopathy/LibraryViewController.m 很遗憾苹果的SDK缺少这样一个基本功能,每个人都必须绕过它。 - catamphetamine
1
我只需要一个技巧就能让它工作。@EricBaker的提示最终解决了问题。感谢你的分享。现在我的sizingCell或其contentView都有非零大小了。 - MkVal
1
对于那些遇到高度不正确的问题的人,请确保您的sizingCell的宽度与您的tableView的宽度匹配。 - MkVal
显示剩余13条评论

30
Eric Baker的评论启示了我核心思想,即为了使视图的大小由其中放置的内容确定,则其中放置的内容必须与包含视图有明确的关系,以动态地驱动其高度(或宽度)。 "添加子视图"并不会创建这种关系,正如您可能会认为的那样。您必须选择哪个子视图将驱动容器的高度和/或宽度...通常是您在整个UI的右下角放置的任何UI元素。以下是一些代码和内联注释来说明这一点。

请注意,对于那些使用滚动视图工作的人来说,这可能特别有价值,因为通常会围绕一个单个内容视图设计,该视图根据放入其中的任何内容动态地确定其大小(并将此信息传达给滚动视图)。祝你好运,希望这能帮助到某个人。

//
//  ViewController.m
//  AutoLayoutDynamicVerticalContainerHeight
//

#import "ViewController.h"

@interface ViewController ()
@property (strong, nonatomic) UIView *contentView;
@property (strong, nonatomic) UILabel *myLabel;
@property (strong, nonatomic) UILabel *myOtherLabel;
@end

@implementation ViewController

- (void)viewDidLoad
{
    // INVOKE SUPER
    [super viewDidLoad];

    // INIT ALL REQUIRED UI ELEMENTS
    self.contentView = [[UIView alloc] init];
    self.myLabel = [[UILabel alloc] init];
    self.myOtherLabel = [[UILabel alloc] init];
    NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(_contentView, _myLabel, _myOtherLabel);

    // TURN AUTO LAYOUT ON FOR EACH ONE OF THEM
    self.contentView.translatesAutoresizingMaskIntoConstraints = NO;
    self.myLabel.translatesAutoresizingMaskIntoConstraints = NO;
    self.myOtherLabel.translatesAutoresizingMaskIntoConstraints = NO;

    // ESTABLISH VIEW HIERARCHY
    [self.view addSubview:self.contentView]; // View adds content view
    [self.contentView addSubview:self.myLabel]; // Content view adds my label (and all other UI... what's added here drives the container height (and width))
    [self.contentView addSubview:self.myOtherLabel];

    // LAYOUT

    // Layout CONTENT VIEW (Pinned to left, top. Note, it expects to get its vertical height (and horizontal width) dynamically based on whatever is placed within).
    // Note, if you don't want horizontal width to be driven by content, just pin left AND right to superview.
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[_contentView]" options:0 metrics:0 views:viewsDictionary]]; // Only pinned to left, no horizontal width yet
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[_contentView]" options:0 metrics:0 views:viewsDictionary]]; // Only pinned to top, no vertical height yet

    /* WHATEVER WE ADD NEXT NEEDS TO EXPLICITLY "PUSH OUT ON" THE CONTAINING CONTENT VIEW SO THAT OUR CONTENT DYNAMICALLY DETERMINES THE SIZE OF THE CONTAINING VIEW */
    // ^To me this is what's weird... but okay once you understand...

    // Layout MY LABEL (Anchor to upper left with default margin, width and height are dynamic based on text, font, etc (i.e. UILabel has an intrinsicContentSize))
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_myLabel]" options:0 metrics:0 views:viewsDictionary]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-[_myLabel]" options:0 metrics:0 views:viewsDictionary]];

    // Layout MY OTHER LABEL (Anchored by vertical space to the sibling label that comes before it)
    // Note, this is the view that we are choosing to use to drive the height (and width) of our container...

    // The LAST "|" character is KEY, it's what drives the WIDTH of contentView (red color)
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-[_myOtherLabel]-|" options:0 metrics:0 views:viewsDictionary]];

    // Again, the LAST "|" character is KEY, it's what drives the HEIGHT of contentView (red color)
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:[_myLabel]-[_myOtherLabel]-|" options:0 metrics:0 views:viewsDictionary]];

    // COLOR VIEWS
    self.view.backgroundColor = [UIColor purpleColor];
    self.contentView.backgroundColor = [UIColor redColor];
    self.myLabel.backgroundColor = [UIColor orangeColor];
    self.myOtherLabel.backgroundColor = [UIColor greenColor];

    // CONFIGURE VIEWS

    // Configure MY LABEL
    self.myLabel.text = @"HELLO WORLD\nLine 2\nLine 3, yo";
    self.myLabel.numberOfLines = 0; // Let it flow

    // Configure MY OTHER LABEL
    self.myOtherLabel.text = @"My OTHER label... This\nis the UI element I'm\narbitrarily choosing\nto drive the width and height\nof the container (the red view)";
    self.myOtherLabel.numberOfLines = 0;
    self.myOtherLabel.font = [UIFont systemFontOfSize:21];
}

@end

How to resize superview to fit all subviews with autolayout.png


3
这是一个很好的技巧,但鲜为人知。重申一遍:如果内部视图具有固有高度并被固定在顶部和底部,那么外部视图不需要指定其高度,实际上会自动适应内容。您可能需要调整内部视图的内容压缩和自适应大小以获得所需的结果。 - phatmann
这真是太棒了!这是我见过的更好的简明 VFL 示例之一。 - Evan R
这正是我在寻找的(感谢@John),因此我已经在这里发布了Swift版本 here - James
1
你必须选择哪个子视图将驱动容器的高度和/或宽度...通常是您放置在整体UI右下角的任何UI元素。我正在使用PureLayout,这对我很重要。对于一个父视图,有一个子视图,将子视图设置为固定到右下角,突然给了父视图一个大小。谢谢! - Steven Elliott
2
选择一个视图来驱动宽度正是我不能做的。有时候一个子视图更宽,有时候另一个子视图更宽。针对这种情况有什么建议吗? - Henning
谢谢您提供这么详细的解释,这帮助我理解了问题的本质并解决了它。 - Viktor Malyi

3
你可以通过创建约束并通过接口构建器连接来实现此目的。
请参阅说明:Interface Builder中的Auto_Layout_Constraints raywenderlich beginning-auto-layout AutolayoutPG Articles constraint Fundamentals
@interface ViewController : UIViewController {
    IBOutlet NSLayoutConstraint *leadingSpaceConstraint;
    IBOutlet NSLayoutConstraint *topSpaceConstraint;
}
@property (weak, nonatomic) IBOutlet NSLayoutConstraint *leadingSpaceConstraint;

将此约束出口连接到您的子视图约束或超级视图约束,并根据您的要求进行设置,如下所示。

 self.leadingSpaceConstraint.constant = 10.0;//whatever you want to assign

我希望这可以澄清它。

因此,可以从源代码配置在XCode中创建的约束。但问题是如何将它们配置为调整超级视图大小。 - DAK
是的 @DAK。请确保父视图的布局。在父视图上添加约束并设置高度约束,这样无论子视图增加多少,它都会自动增加父视图的约束高度。 - chandan
这对我也起作用了。我的单元格大小是固定的(高度为60px),这很有帮助。因此,当视图加载时,我将我的IBOutlet高度约束设置为60 * x,其中x是单元格数量。最终,您可能希望将其放在滚动视图中,以便可以查看整个内容。 - Sandy Chapman

-1

这可以针对较大的UIView中的普通subview完成,但是对于headerViews,它不会自动工作。 headerView的高度由tableView:heightForHeaderInSection:返回的内容确定,因此您必须根据UILabelheight以及所需的UIButton和任何padding来计算height。 您需要执行以下操作:

-(CGFloat)tableView:(UITableView *)tableView 
          heightForHeaderInSection:(NSInteger)section {
    NSString *s = self.headeString[indexPath.section];
    CGSize size = [s sizeWithFont:[UIFont systemFontOfSize:17] 
                constrainedToSize:CGSizeMake(281, CGFLOAT_MAX)
                    lineBreakMode:NSLineBreakByWordWrapping];
    return size.height + 60;
}

在这里headerString是你想填充UILabel的任意字符串,而281是UILabelwidth(如在Interface Builder中设置)


很遗憾,这个方法不起作用。在父视图上使用“自适应内容大小”会移除将按钮底部与父视图连接的约束,正如您所预测的那样。在运行时,标签被重新调整大小并将按钮向下推动,但父视图并没有自动调整大小。 - DAK
@DAK,抱歉,问题在于您的视图是表视图的标题。我误解了您的情况。我原本认为包含按钮和标签的视图位于另一个视图内部,但听起来您正在将其用作标题(而不是标题内部的视图)。因此,我的原始答案无法使用。我已更改我的答案,以我认为应该有效的方式。 - rdelmar
这与我的当前实现类似,但我希望有更好的方法。我宁愿避免每次更改标题时更新此代码。 - DAK
@Dak,抱歉,我认为没有更好的方法,那就是表视图的工作方式。 - rdelmar
请查看我的回答。"更好的方法"是使用UIView systemLayoutSizeFittingSize:。尽管如此,仅仅为了在表格视图中获取所需高度而对视图或单元格执行完全自动布局相当昂贵。我会为复杂的单元格执行此操作,但对于简单情况可能会退回到像您的解决方案这样的解决方案。 - TomSwift

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