刚开始使用 Autolayout 遇到下面的警告人容易让人气馁,经常不知所措而放弃了使用 Autolayout。
Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want.
Try this:
(1) look at each constraint and try to figure out which you don't expect;
(2) find the code that added the unwanted constraint or constraints and fix it.
(Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints)
(...........)
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.
正如输出中所述,Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger
,现在介绍下使用 UIViewAlertForUnsatisfiableConstraints
的调试方法。
在 UIViewAlertForUnsatisfiableConstraints
添加 symbolic breakpoint
:
image.png
再次调试的时候就可以通过 lldb 来调试了,然并卵,如果你不知道 lldb 的话。
所以交给你一个小技巧,添加
po [[UIWindow keyWindow] _autolayoutTrace] // OC项目
expr -l objc++ -O -- [[UIWindow keyWindow] _autolayoutTrace] // Swift项目
image.png
这样就可以直接看到输出:
(lldb) po [[UIWindow keyWindow] _autolayoutTrace]
UIWindow:0x7f9481c93360
| •UIView:0x7f9481c9d680
| | *UIView:0x7f9481c9d990- AMBIGUOUS LAYOUT for UIView:0x7f9481c9d990.minX{id: 13}, UIView:0x7f9481c9d990.minY{id: 16}
| | *_UILayoutGuide:0x7f9481c9e160- AMBIGUOUS LAYOUT for _UILayoutGuide:0x7f9481c9e160.minY{id: 17}
| | *_UILayoutGuide:0x7f9481c9ebb0- AMBIGUOUS LAYOUT for _UILayoutGuide:0x7f9481c9ebb0.minY{id: 27}
其中 AMBIGUOUS 相关的视图就是约束有问题的。0x7f9481c9d990就是有问题视图的首地址。
当然进一步的调试需要 lldb 的命令。比如,打印视图对象
(lldb) po 0x7f9481c9d990
<UIView: 0x7f9481c9d990; frame = (0 0; 768 359); autoresize = RM+BM; layer = <CALayer: 0x7fc82d338960>>
改变颜色:
(lldb) expr ((UIView *)0x174197010).backgroundColor = [UIColor redColor]
(UICachedDeviceRGBColor *) $4 = 0x0000000174469cc0
剩下的就是去代码中找到这个视图,然后修改其约束了。
UIView 是我们经常使用的一个基本控件,其中有几个基本的布局方法需要清楚。
当 View
及其所有子视图的 frame
发生改变的时候,会调用 layoutSubviews
,所以在需要更新 frame 来重新定位或更改大小时重载它。这个方法很开销很大,因为它会在每个子视图上起作用并且调用它们相应的 layoutSubviews
方法。注意:最好不要在代码中手动调用 layoutSubviews
方法。当 layoutSubviews
完成后,在 view
的所有者 view controller
上,会触发 viewDidLayoutSubviews
调用。因为 viewDidLayoutSubviews
是 view
布局更新后会被唯一可靠调用的方法,所以你应该把所有依赖于布局或者大小的代码放在 viewDidLayoutSubviews
中,而不是放在 viewDidLoad
或者 viewDidAppear
中。
触发 layoutSubviews
的时机:
addSubview
方法会触发 layoutSubviews
。view
的 Frame
发生变化也会触发layoutSubviews
。UIScrollView
会触发 layoutSubviews
。View
上的 layoutSubviews
。View
大小的时候也会触发父 View
上的 layoutSubviews
。layoutSubviews
调用的最省资源的方法就是在你的视图上调用 setNeedsLaylout
方法,表示视图的布局需要重新计算。告知页面需要更新,但是不会立刻开始更新视图,视图会在下一个 runloop
中更新,调用 setNeedsLaylout
方法视图被重新绘制并布局之间会有一段任意时间的间隔。layoutIfNeeded
会触发 layoutSubviews
,告知页面布局立刻更新,所以一般都会和 setNeedsLayout
一起使用。如果希望立刻生成新的 frame
需要调用此方法,利用这点一般布局动画可以在更新布局后直接使用这个方法让动画生效。runloop
中更新约束,通过标记 update constraints
来触发 updateConstraints
。layoutIfNeeded
等价。它会检查 update constraints
标记。如果认为这些约束需要被更新,它会立即触发 updateConstraints
,而不会等到 run loop
的末尾。updateConstraints
方法。通常在 updateConstraints
方法中实现必须要更新的约束,在设置或者解除约束、更改约束的优先级或者常量值,或者从视图层级中移除一个视图时都会设置一个内部的标记 update constarints
,这个标记会在下一个更新周期中触发调用 updateConstrains
。注意:layoutSubViews 在 drawRect 之前调用。
在使用 AutoLayout 的时候可能也会同时也会用到 frame,比如需要用到 layer 的时候,想让 layer 的尺寸是由其它视图尺寸设定的,而这个视图又是由约束控制布局的,如果将 layer 的初始化与 view 的初始化放在一个方法中; 比如:
layer.bounds = CGRectMake(0,0,view.bounds.size.widith * 0.5,50)
那么很可能拿到 layer 的宽度是0。
比如:
UIView *redView = [[UIView alloc] init];
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];
self.redView = redView;
// 设置约束
[redView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view.mas_centerX);
make.centerY.equalTo(self.view.mas_centerY);
make.size.mas_equalTo(CGSizeMake(150, 80));
}];
NSLog(@"self.view 的尺寸%@,redView 的尺寸%@",self.view,redView);
2017-06-08 15:32:51.815107+0800 MasonryDemo[42940:1076244] self.view 的尺寸<UIView: 0x7fd8cd408960; frame = (0 0; 414 736); autoresize = W+H; layer = <CALayer: 0x604000227200>>,redView 的尺寸<UIView: 0x7fd8cd407650; frame = (0 0; 0 0); layer = <CALayer: 0x6040002274a0>>
这个时候,看到为什么设置了约束,而打印出来的 frame 是 (0 0; 0 0),是因为约束被设置之后它并不会立即对 view 作出改变,而是要等到 layout 时,才会对视图的尺寸进行修改,而 layout 通常是在视图已经加载到父视图上面时做出响应。
所以如果在 viewDidLoad 中设置了约束,那么要等到 viewDidAppear 时 view 的尺寸才会真正改变。
解决办法:
- (void)testLayout {
UIView *redView = [[UIView alloc] init];
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];
self.redView = redView;
// 设置约束
[redView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view.mas_centerX);
make.centerY.equalTo(self.view.mas_centerY);
make.size.mas_equalTo(CGSizeMake(150, 80));
}];
}
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
NSLog(@"self.view 的尺寸%@,redView 的尺寸%@",self.view,self.redView);
}
2017-06-08 15:50:41.621147+0800 MasonryDemo[43363:1089098] self.view 的尺寸<UIView: 0x7fe412f0f780; frame = (0 0; 414 736); autoresize = W+H; layer = <CALayer: 0x604000238b00>>,redView 的尺寸<UIView: 0x7fe412e045b0; frame = (132 328; 150 80); layer = <CALayer: 0x60000003c460>>
1、把获取 frame 的设置写到 layoutSubviews 中或者写到 viewDidLayoutSubviews 中即可。因为 layout 约束生效时 view 的 center 或者 bounds 就会被修改,当 center 或者 bounds 被修改时layoutSubview 就会被调用,随后 viewDidLayoutSubviews 就回被调用。这个时候,设置约束的视图 frame 就不再是 (0,0,0,0) 了。
- (void)testLayout {
UIView *redView = [[UIView alloc] init];
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];
self.redView = redView;
// 设置约束
[redView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view.mas_centerX);
make.centerY.equalTo(self.view.mas_centerY);
make.size.mas_equalTo(CGSizeMake(150, 80));
}];
[redView setNeedsLayout];
[redView layoutIfNeeded];
NSLog(@"self.view 的尺寸%@,redView 的尺寸%@",self.view,redView);
}
2017-06-08 15:52:32.749105+0800 MasonryDemo[43419:1090641] self.view 的尺寸<UIView: 0x7fe36440b5f0; frame = (0 0; 414 736); autoresize = W+H; layer = <CALayer: 0x604000422100>>,redView 的尺寸<UIView: 0x7fe364405040; frame = (-75 -40; 150 80); layer = <CALayer: 0x6040004207a0>>
2、如果将约束和 frame 写在同一方法中,写完约束就设置 frame,而不是想把 frame 的设置放到 layoutSubview 中,比如设置好约束后马上就想根据约束的结果计算高度,那么必须在设置完约束之后手动调用 setNeedsLayout 和 layoutIfNeeded 方法,让视图立即 layout,更新 frame,但是这个时候就可以拿到真实的 size 并不能拿到真实的 center ,不建议这么使用。
- (void)testLayout {
UIView *redView = [[UIView alloc] init];
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];
self.redView = redView;
// 设置约束
[redView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(self.view.mas_centerX);
make.centerY.equalTo(self.view.mas_centerY);
make.size.mas_equalTo(CGSizeMake(150, 80));
}];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"self.view 的尺寸%@,redView 的尺寸%@",self.view,redView);
});
}
2017-06-08 15:55:56.282546+0800 MasonryDemo[43500:1092911] self.view 的尺寸<UIView: 0x7fda85e0d540; frame = (0 0; 414 736); autoresize = W+H; layer = <CALayer: 0x600000233620>>,redView 的尺寸<UIView: 0x7fda85e0c770; frame = (132 328; 150 80); layer = <CALayer: 0x600000233540>>
3、在 dispatch_after 里面可以拿到真实的 frame ,或许是因为设置约束和获取 frame 不在同一个 runloop 的原因吧。