创build一个基于自动布局的度量视图

我有一个可重用的视图,我将在UITableViewCell'sUICollectionViewCell使用,并且需要为tableView:heightForRowAtIndexPath:获取其维度。 有些子视图在layoutSubviews里面有东西,所以我不能调用systemLayoutForContentSize:而是我的计划是:

  1. 实例化度量视图。
  2. 设置大小以包含所需的宽度。
  3. 用数据填充它。
  4. 更新约束/布局子视图。
  5. 抓住视图的高度或内部“尺寸”视图。

我遇到的问题是,我不能强制视图布局没有插入到视图中,等待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最后表明,添加到视图层次结构和一个通过主循环布局的视图。

我希望能够得到视图的维度,而不必让应用程序自己处理它或执行手动计算(string高度+ constraint1 + constraint2)。

更新

我观察到,如果我把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 ,则视图的直接子视图将被布置,但是他们的子项都不是。

Autolayout问题

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

layoutIfNeeded上的UIView类的引用 :

使用此方法在绘制之前强制子视图的布局。 从接收器开始,只要superview需要布局,这个方法就会遍历视图层次结构。 然后把那棵树下的整棵树放在那个祖先的下面。 因此,调用此方法可能会强制整个视图层次结构的布局。 这个的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:计算heightForRowAtIndexPath:可能会很昂贵,所以你可能想要预先计算和caching结果
  • 预先计算应该在后台线程上完成,但UIView布局不能正常工作,除非在主线程上完成
  • 您应该明确实施estimatedHeightForRowAtIndexPath:来减less这些性能问题的影响

使用intrinsicContentSize

回应:

有些子视图在layoutSubviews里面有东西,所以我不能调用systemLayoutForContentSize:

如果实现intrinsicContentSize ,则可以使用此方法,这可以使视图为自己提供最佳大小。 一个这样的实现可能是:

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

如果你的layoutSubviews方法没有引用一个已经设置的大小(比如self.bounds或者self.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)); } 

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

最后,我还要补充一点是,除了最简单的表格单元之外,所有这些都是使用自动布局的指数级增加的成本 ,通常我使用手动高度计算。

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

2013年12月30日更新:在testing了Aaron Brager的示例代码之后,我现在意识到前面的段落是不正确的。 显然,你可以通过调用setNeedsLayoutlayoutIfNeeded强制在viewDidLoad进行布局。

如果您执行演示代码来响应点击button,我想您会看到操作方法完成之前logging的嵌套子视图的最终几何graphics。

 - (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); } 

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

但在视图控制器的初始设置过程中,您将如何获得可重用子视图的最终几何graphics?

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

例如,如果子视图的内容是属性string,则可以使用像boundingRectWithSize:options:context:这样的方法来帮助确定子视图的大小。

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