现在BNRDetailViewController.xib中的所有视图都添加了约束,自动布局系统也为所有视图定义了对齐矩形,因此BNRDetailViewController的view在iPhone和iPad中的显示效果都很好。
在添加大量约束的过程中,很容易产生问题,例如缺少约束、约束冲突和视图位置错误。自动布局系统提供了若干用于调试约束问题的工具,下面就依次介绍这些工具以及如何使用这些工具调试约束问题。
有歧义的布局
有歧义的布局(ambiguous layout)是指自动布局系统无法根据当前约束确定视图的所有布局属性,该问题通常是由于视图缺少约束。
目前BNRDetailViewController.xib中没有缺少约束的视图,下面就来演示这类问题。首先在UIControl中的date标签下方添加两个标签,分别位于屏幕左侧和右侧。然后打开属性检视面板,将两个标签的背景颜色改为浅灰色,以便查看它们的frame,界面应该类似图15-16。
图15-16 添加两个新标签
接下来为两个标签添加一些约束。按住Shift键,同时选中两个标签,再打开Pin菜单,在菜单顶部选择顶边、左边和右边。最后点击Add 5 Constraints添加约束(见图15-17)。
图15-17 同时为两个标签添加约束
读者可能会问,为两个标签各添加了三个方向上的约束,为什么按钮提示添加5个约束(Add 5 Constraints),而不是6个呢?这是因为,左侧标签右边方向的约束和右侧标签左边方向的约束是同一个约束,Interface Builder会自动识别这类重复约束并只会添加一次。
如果现在构建并运行应用,BNRDetailViewController的view在iPhone中的显示效果很好,但是在iPad中,其中一个标签会比另一个标签更宽,如图15-18所示。
图15-18 在iPad中,其中一个标签比另一个更宽
这两个标签目前缺少约束,自动布局系统无法确定它们的所有布局属性,只能在应用运行时根据其他约束推测某些布局属性的值,因此BNRDetailViewController的view在iPad上没有按照期望的方式正确布局。下面介绍如何使用UIView的两个方法:hasAmbiguousLayout和exerciseAmbiguousLayout来调试这类问题。
打开BNRDetailViewController.m,覆盖viewDidLayoutSubviews方法,检查其view中是否存在有歧义布局的子视图。
- (void)viewDidLayoutSubviews
{
for (UIView *subview in self.view.subviews) {
if([subview hasAmbiguousLayout])
NSLog(@“AMBIGUOUS: %@”, subview);
}
}
当UIViewController的view首次出现在屏幕上或frame发生变化(例如旋转设备)时,UIViewController就会收到viewDidLayoutSubviews消息。在iPad模拟器上构建并运行应用,进入BNRDetailViewController,然后查看控制台中的输出。可以看见,两个标签存在有歧义的布局。
为了进一步知道视图缺少哪种约束,可以查看自动布局系统推测的另一种布局方式。在BNRDetailViewController.m中修改backgroundTapped:方法,向有歧义布局的子视图发送exerciseAmbiguityInLayout消息。
- (IBAction)backgroundTapped:(id)sender
{
[self.view endEditing:YES];
for (UIView *subview in self.view.subviews) {
if ([subview hasAmbiguousLayout]) {
[subview exerciseAmbiguityInLayout];
}
}
}
构建并运行应用,进入BNRDetailViewController,然后点击视图任意位置,这时会发现宽度较窄的标签会变宽,而较宽的标签会变窄。如果再次点击,两个标签的宽度就会恢复原状(见图15-19)。
图15-19 自动布局系统推测的另一种布局方式
由于两个标签的宽度都没有添加约束,因此自动布局系统提供了两种布局效果,点击视图就会在这两种效果之间切换。为了消除歧义,需要为其中一个标签添加宽度约束。只要确定了一个标签的宽度,自动布局系统就能同时确定另一个标签的宽度。这里有一种很好的解决方案:让两个标签的宽度保持相等。
打开BNRDetailViewController.xib,按住Control键,将其中一个标签拖曳到另一个标签,然后选择Equal Widths(宽度相等)。在iPad上构建并运行应用,会发现两个标签宽度相等,控制台中也不会输出有歧义布局的子视图信息了。点击背景,界面也不会再发生变化。
现在界面已经没有约束错误了,自动布局系统为所有视图都定义了对齐矩形,界面只有唯一的布局效果。
在BNRDetailViewController.xib中,选中并删除用于演示约束问题的两个标签。
请读者记住,exerciseAmbiguityInLayout方法仅仅是用来调试约束问题的工具,用来查看自动布局系统在有歧义布局情况下的各种布局效果——发布应用时,不要使用该方法。
在BNRDetailViewController.m中,删除backgroundTapped:中调用exercise- AmbiguityInLayout的代码以及viewDidLayoutSubviews。
- (void)viewDidLayoutSubviews
{
for (UIView *subview in self.view.subviews) {
if([subview hasAmbiguousLayout])
NSLog(@/"AMBIGUOUS: %@/", subview);
}
}
- (IBAction)backgroundTapped:(id)sender
{
[self.view endEditing:YES];
for (UIView *subview in self.view.subviews) {
if ([subview hasAmbiguousLayout]) {
[subview exerciseAmbiguityInLayout];
}
}
}
无法满足的约束
如果为视图添加了不必要的约束,就可能造成多个约束之间发生冲突,自动布局系统无法同时满足这些约束。下面在BNRDetailViewController中演示这类问题。
打开BNRDetailViewController.xib,选中date标签,然后在属性检视面板中将背景颜色改为浅灰色,以便查看其frame。接下来在Pin菜单中限定标签的宽度始终为当前值。
在iPhone中构建并运行应用,没有问题。再切换到iPad,界面看起来也没有问题,但是控制台会输出类似以下内容:
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)
(
“<NSLayoutConstraint:0xa333da0 H:[UILabel:0xa333ca0(280)]>”,
“<NSLayoutConstraint:0xa394500 H:[UILabel:0xa333ca0]-(20)-|
(Names: /'|/':UIControl:0xa38cd80 )>”,
“<NSLayoutConstraint:0xa394530 H:|-(20)-[UILabel:0xa333ca0]
(Names: /'|/':UIControl:0xa38cd80 )>”,
“<NSAutoresizingMaskLayoutConstraint:0xa3a1a70 h=-&-
v=-&- UIControl:0xa38cd80.width ==
_UIParallaxDimmingView:0xa37b140.width>”,
“<NSAutoresizingMaskLayoutConstraint:0xa3a21d0 h=--&
v=--& H:[_UIParallaxDimmingView:0xa37b140(768)]>”
)
Will attempt to recover by breaking constraint
<NSLayoutConstraint:0xa333da0 H:[UILabel:0xa333ca0(280)]>
Break on objc_exception_throw to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed
in <UIKit/UIView.h> may also be helpful.
首先,Xcode提示“unable to simultaneously satisfy constraints(无法同时满足所有约束)”并给出了两条建议:
(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.(查看代码中是否添加了不需要的约束并删除相应代码。)
接下来是与问题相关的所有约束,最后Xcode会报告忽略了哪些约束,以便解决冲突。在这里,Xcode忽略了date标签的宽度约束。
下面请读者思考约束冲突的原因。之前为date标签添加了左边和右边两个方向上的约束,现在又限定其宽度为固定值,因此产生了冲突。解决方法是删除左边约束、右边约束和宽度约束中的任意一个。
在BNRDetailViewController.xib中删除date标签的宽度约束,然后设置其背景颜色为透明(Clear Color)。
视图位置错误
如果视图在XIB文件中的frame与自身约束不一致,就会发生视图位置错误。视图位置错误是指视图在运行时的frame与其在画布中的frame不同。下面仍然在BNRDetailViewController中演示这类问题。
选中date标签并将其向下拖曳一定的距离,这时界面应该类似图15-20。
图15-20 位置错误的视图
标签原来的位置会出现一个橘红色虚线边框的矩形,这是标签在运行时的frame。自动布局系统在运行时会根据约束将标签移动到该矩形的位置,而不是刚才拖曳后的位置。
这类错误有两种解决方案,取决于视图在画布上的frame是否符合要求。如果要求视图在运行时的frame与画布上的frame相同,就修改视图的约束,匹配当前frame;反之,就修改视图的大小或位置匹配当前约束。对于date标签,画布上的frame不符合要求,需要移动date标签匹配当前约束。
选中date标签,然后在画布右下角的约束菜单中点击图标,显示Resolve Auto Layout Issues(解决自动布局问题)菜单,如图15-21所示。
图15-21 Resolve Auto Layout Issues菜单
在菜单顶部选择Update Frames(更新frame属性),date标签会恢复原来位置,与当前约束相匹配。
相反,如果需要修改约束匹配当前frame,就选择Update Constraints(更新约束)。
下半部分菜单与上半部分的功能基本相同,只是上半部分菜单仅仅会操作选中的视图,而下半部分菜单会操作界面上的所有视图。