创build一个基于自动布局的度量视图
我有一个可重用的视图,我将在UITableViewCell's
和UICollectionViewCell
使用,并且需要为tableView:heightForRowAtIndexPath:
获取其维度。 有些子视图在layoutSubviews
里面有东西,所以我不能调用systemLayoutForContentSize:
而是我的计划是:
- 实例化度量视图。
- 设置大小以包含所需的宽度。
- 用数据填充它。
- 更新约束/布局子视图。
- 抓住视图的高度或内部“尺寸”视图。
我遇到的问题是,我不能强制视图布局没有插入到视图中,等待runloop。
我已经提炼出一个相当无聊的例子。 这里是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的示例代码之后,我现在意识到前面的段落是不正确的。 显然,你可以通过调用setNeedsLayout
和layoutIfNeeded
强制在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];