创建基于autolayout的指标视图

时间:2013-12-20 23:25:47

标签: ios iphone uikit autolayout

我将在UITableViewCell'sUICollectionViewCell中使用可重复使用的视图,并且需要获取tableView:heightForRowAtIndexPath:的维度。一些子视图在layoutSubviews内有内容,因此我无法致电systemLayoutForContentSize:,而我的计划是:

  1. 实例化指标视图。
  2. 设置尺寸以包含所需的宽度。
  3. 用数据填充。
  4. 更新约束/布局子视图。
  5. 抓住视图的高度或内部“大小调整”视图。
  6. 我遇到的问题是,我不能强制视图布局而不将其插入视图并等待runloop。

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

    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,则会显示视图的直接子视图,但不会显示他们的子视图。

2 个答案:

答案 0 :(得分:5)

Autolayout问题

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

来自UIView class reference on layoutIfNeeded

  

使用此方法在绘制前强制子视图的布局。从接收者开始,只要superviews需要布局,此方法就会向上遍历视图层次结构。然后它展示了祖先下面的整棵树。因此,调用此方法可能会强制整个视图层次结构的布局。 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,除了最简单的表格单元格外,我通常最终使用手动高度计算。

答案 1 :(得分:3)

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

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

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

- (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];