创建基于自动布局的指标视图

Posted

技术标签:

【中文标题】创建基于自动布局的指标视图【英文标题】:Creating an autolayout-based metrics view 【发布时间】:2013-12-20 23:25:47 【问题描述】:

我有一个将在UITableViewCell'sUICollectionViewCell 中使用的可重用视图,并且需要获取tableView:heightForRowAtIndexPath: 的尺寸。一些子视图在layoutSubviews 内部发生了一些事情,所以我不能打电话给systemLayoutForContentSize:,而是我的计划是:

    实例化指标视图。 设置大小以包含所需的宽度。 用数据填充它。 更新约束/布局子视图。 获取视图的高度或内部“尺寸”视图。

我遇到的问题是,如果不将视图插入视图并等待运行循环,我无法强制视图进行布局。

我提炼了一个相当无聊的例子。这是 View.xib。子视图未对齐以突出显示该视图从未布局到基线位置:

在我调用的主线程上:

UIView *view = [[UINib nibWithNibName:@"View" bundle:nil] instantiateWithOwner:nil options:nil][0];

NSLog(@"Subviews: %@", view.subviews);
view.frame = CGRectMake(0, 0, 200, 200);

[view updateConstraints];
[view layoutSubviews];
NSLog(@"Subviews: %@", view.subviews);

[self.view addSubview:view];
[view updateConstraints];
[view layoutSubviews];
NSLog(@"Subviews: %@", view.subviews);

dispatch_async(dispatch_get_main_queue(), ^
    NSLog(@"Subviews: %@", view.subviews);
);

我得到以下视图信息:

1) "<UIView: 0x8bad9e0; frame = (50 50; 220 468); autoresize = W+H; layer = <CALayer: 0x8be0070>>"
2) "<UIView: 0x8bad9e0; frame = (50 50; 220 468); autoresize = W+H; layer = <CALayer: 0x8be0070>>"
3) "<UIView: 0x8bad9e0; frame = (50 50; 220 468); autoresize = W+H; layer = <CALayer: 0x8be0070>>"
4) "<UIView: 0x8bad9e0; frame = (0 100; 100 100); autoresize = W+H; layer = <CALayer: 0x8be0070>>"

1 表示全新的 NIB 视图尚未布局。 2 表示updateConstraints/layoutSubviews 什么也没做。 3 表示将其添加到视图层次结构中没有任何作用。 4 最后表示添加到视图层次结构和一个通过主循环布局视图。

我希望能够获得视图的尺寸,而不必让应用程序处理它或自己执行手动计算(字符串高度 + 约束 1 + 约束 2)。

更新

我观察到,如果我将 view 放在 UIWindow 中,我会得到轻微的改进:

UIView *view = [[UINib nibWithNibName:@"View" bundle:nil] instantiateWithOwner:nil options:nil][0];
UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
view.frame = CGRectMake(0, 0, 200, 200);
[window addSubview:view];
[view layoutSubviews];

如果view.translatesAutoresizingMaskIntoConstraints == YES,视图的直接子视图将被布局,但不会布局它们的子视图。

【问题讨论】:

【参考方案1】:

自动布局问题

在您提到的基本情况下,您可以通过在容器视图上调用setNeedsLayout 然后layoutIfNeeded 来获得正确的大小。

来自UIView class reference on layoutIfNeeded

使用此方法在绘制之前强制子视图的布局。从接收者开始,只要超级视图需要布局,此方法就会向上遍历视图层次结构。然后它把整棵树放在那个祖先的下面。因此,调用此方法可能会强制整个视图层次结构的布局。此 UIView 实现调用等效的 CALayer 方法,因此具有与 CALayer 相同的行为。

我认为“整个视图层次结构”不适用于您的用例,因为指标视图可能没有超级视图。

示例代码

在一个示例空项目中,仅使用此代码,在调用layoutIfNeeded 后确定正确的帧:

#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, strong) UIView *redView;

@end

@implementation ViewController

@synthesize redView;

- (void)viewDidLoad

    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    redView = [[UIView alloc] initWithFrame:CGRectMake(50, 50, 220, 468)];
    redView.backgroundColor = [UIColor redColor];
    redView.translatesAutoresizingMaskIntoConstraints = NO;
    [self.view addSubview:redView];

    NSLog(@"Red View frame: %@", NSStringFromCGRect(redView.frame));
    // outputs "Red View frame: 50, 50, 220, 468"

    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[redView(==100)]" options:0 metrics:Nil views:NSDictionaryOfVariableBindings(redView)]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-100-[redView(==100)]" options:0 metrics:Nil views:NSDictionaryOfVariableBindings(redView)]];

    NSLog(@"Red View frame: %@", NSStringFromCGRect(redView.frame));
    // outputs "Red View frame: 50, 50, 220, 468"

    [self.view setNeedsLayout];

    NSLog(@"Red View frame: %@", NSStringFromCGRect(redView.frame));
    // outputs "Red View frame: 50, 50, 220, 468"

    [self.view layoutIfNeeded];

    NSLog(@"Red View frame: %@", NSStringFromCGRect(redView.frame));
    // outputs "Red View frame: 0, 100, 100, 100"


@end

其他注意事项

稍微超出了您的问题范围,以下是您可能会遇到的其他一些问题,因为我已经在一个真实的应用程序中解决了这个确切的问题:

heightForRowAtIndexPath: 中计算可能会很昂贵,因此您可能需要预先计算并缓存结果 预计算应该在后台线程上完成,但 UIView 布局不能很好地工作,除非它在主线程上完成 您绝对应该实施estimatedHeightForRowAtIndexPath: 以减少这些性能问题的影响

使用intrinsicContentSize

回应:

一些子视图在layoutSubviews 内部发生了一些事情,所以我不能调用systemLayoutForContentSize:

如果你实现了intrinsicContentSize,你可以使用这个方法,它可以让视图为自己建议一个最佳尺寸。一种实现可能是:

- (CGSize) intrinsicContentSize 
    [self layoutSubviews];
    return CGSizeMake(CGRectGetMaxX(self.bottomRightSubview.frame), CGRectGetMaxY(self.bottomRightSubview.frame));

这种简单的方法只有在您的 layoutSubviews 方法不引用已设置的大小(如 self.boundsself.frame)时才有效。如果是这样,您可能需要执行以下操作:

- (CGSize) intrinsicContentSize 
    self.frame = CGRectMake(0, 0, 10000, 10000);

    while ([self viewIsWayTooLarge] == YES) 
        self.frame = CGRectInset(self.frame, 100, 100);
        [self layoutSubviews];
    

    return CGSizeMake(CGRectGetMaxX(self.bottomRightSubview.frame), CGRectGetMaxY(self.bottomRightSubview.frame));

显然,您需要调整这些值以匹配每个视图的特定布局,并且您可能需要调整性能。

最后,我将部分添加到 exponentially increasing cost of using auto-layout,对于除了最简单的表格单元格之外的所有单元格,我通常最终使用手动高度计算。

【讨论】:

太棒了。我的印象是事情看起来像:-(void)layoutIfNeeded if (self.needsLayout) [self layoutSubviews]; 但你是对的,它是实际处理递归的那个。我不知道intrinsicContentSize 是否会有所帮助,因为我的实际问题是我在标签的layoutSubviews 中更新preferredMaxLayoutWidth 您对性能的看法也是正确的。在分析我的权宜之计解决方案时,我发现大部分时间都花在systemLayoutSizeFittingSize: 上,以获得一些非常无聊的视图,因此尝试为整个事情做这件事会很严重。我已经开始在发送该消息的任何行周围进行大量缓存,因此我最终可能不会在主要指标视图上计算高度。 这很好用。您是如何想到在 viewDidLoad 中调用 setNeedsLayout 后跟 layoutIfNeeded 的?实验? 是的,我基本上在 6 个月前就遇到过这个问题,然后把头发扯掉了 :)【参考方案2】:

大概是在视图控制器首次加载其视图时调用演示代码,例如在viewDidLoad 或其他生命周期方法中。在调用 viewDidLayoutSubviews 之前,嵌套子视图的几何图形不会反映其约束。在视图控制器的初始生命周期中,您所做的任何事情都不会使该方法更快地到达。

12/30/13 更新: 在测试了 Aaron Brager 的示例代码后,我现在意识到上一段是不正确的。显然,您可以通过调用setNeedsLayout 后跟layoutIfNeeded 来强制在viewDidLoad 中布局。

如果您执行演示代码以响应按钮单击,我想您会看到嵌套子视图的最终几何图形在操作方法完成之前记录下来。

- (IBAction)buttonTapped:(id)sender

    UIView *view = [[UINib nibWithNibName:@"View" bundle:nil] instantiateWithOwner:nil options:nil][0];
    view.frame = CGRectMake(0, 0, 200, 200);

    [self.view addSubview:view];
    [self.view layoutIfNeeded];
    NSLog(@"Subviews: %@", view.subviews);

在后一种情况下,您可以按需请求布局,因为视图控制器已完成其初始设置。

但是在视图控制器的初始设置期间,您将如何获得可重用子视图的最终几何图形?

为可重用的子视图设置内容后,让视图控制器询问子视图的大小。换句话说,在您的自定义视图上实现一个根据内容计算大小的方法。

例如,如果子视图的内容是属性字符串,您可以使用boundingRectWithSize:options:context: 之类的方法来帮助确定子视图的大小。

CGRect rect = [attributedString boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX) options:NSStringDrawingUsersLineFragmentOrigin context:nil];

【讨论】:

有趣的是视图渲染是这样延迟的。通过查看您的建议,我想出了一个潜在的解决方法。如果我将视图直接放在UIWindow 中,当我调用layoutSubviews 时它会立即layoutSubviews。似乎有一个规则:if (window &amp;&amp; (!view.isInViewController || view.nearestViewController.hasLaidOutSubviews) doAutoLayoutStuff 不过我不确定在应用程序的整个生命周期内是否会有一个随机隐藏的窗口浮动,所以我会尝试一下。 没关系。看起来这种情况只发生在下一级如果 translatesAutoresizingMaskIntoConstraints 是YES。

以上是关于创建基于自动布局的指标视图的主要内容,如果未能解决你的问题,请参考以下文章

使用自动布局使任何视图的宽度等于屏幕宽度

iOS 7 和 8 中 UITableViewCells 的自动布局指标不同

基于子视图的自动布局高度

我们可以将具有自动布局的 XIB 加载为以编程方式创建的没有自动布局的视图的子视图,并使用父视图调整子视图的大小吗?

使用自动布局自定义集合视图单元格的动态高度

使用堆栈视图和自动布局创建自定义 UITableViewCell