翻译自“Auto Layout Guide”。
3 调试自动布局
3.1 错误类型
自动布局中的错误主要分为三个类型:
- 不可满足的(unsatisfiable)布局。布局没有有效的解。更多信息请参考“不可满足的布局”。
- 有歧义的布局。你的布局有两个或多个可能的解。更多信息请参考“有歧义的布局”。
- 逻辑错误。布局逻辑中存在bug。更多信息请参考“逻辑错误”。
大多数时候,实际问题仅仅是决定什么出错了。你添加了你任务需要的约束,但是运行程序时,并不是你期望的结果。
通常,只要你理解了问题,解决方案就很明显了。移除冲突的约束,添加缺少的约束,以及微调优先级就会解决问题。当然,要容易的理解问题出在哪里,可能需要经理一些试验和错误。更其它技巧一样,孰能生巧。
但有时事情会变得更复杂。此时需要“调试的技巧和窍门”。
3.2 不可满足的布局
当系统不能为当前约束集找到一个有效解时,会产生不可满足的约束。两个或多个必需约束的冲突,因为它们不能同时为真。
3.2.1 识别不可满足的约束
通常,界面生成器可以在设计时检测到冲突。此时,界面生成器有几种方式显示错误:
- 在画布上用红色显示所有冲突的约束。
- Xcode在问题导航器(issue navigator)中作为警告列出所有冲突的约束。
-
界面生成器在文档大纲的右上角显示一个红色箭头。
点击箭头显示当前布局中所有自动布局问题的列表。
自动布局通常会推荐如何修复这些问题。更多信息,请参考“Auto Layout Help”中的“Resolving Layout Issues for a View Controller, Window, or Root View”。
提示
尽管界面生成器提供的实时反馈可以更容易创建有效的布局,但它不能找出所有可能的布局错误。
例如,界面生成器只能在画布的当前尺寸检测冲突;然而有些冲突只发生在根视图被拉伸或压缩到某个点(或者当内容扩展或压缩到某个点)。界面生成器不能检测到这些错误。
所以,尽管你总是修复界面生成器识别的所有问题,但修复明显的错误还是不够的。你还需要执行所有屏幕尺寸,方向,动态类型尺寸(dynamic type sizes)和支持语言的运行时测试(runtime testing)。
当系统在运行时检测到不可满足的布局时,执行以下步骤:
- 自动布局识别冲突约束集。
- 它打破其中一个冲突的约束,并检查布局。系统继续打破约束,直到找到一个有效的布局。
- 自动布局在控制台记录冲突和打破的约束的信息。
这个后备系统让应用程序继续运行,同时尝试显示有意义的东西给用户。但是不同布局,甚至构建,打破约束的效果多种多样。
在很多情况下,缺少约束可能不会有任何可视化效果。视图层次结构按你期望的那样显示。其它情况下,缺少约束可能导致视图层级结构整体错位,尺寸不对,或者完全不显示。
当它们没有明显效果时,通常会暂时忽略错误——毕竟,它们没有改变应用程序的行为。然而,对视图层级结构或者SDK的任何改变也会改变打破的约束集,突然产生一个明显的打破布局。
因此,检测到不可满足的约束时,总是修复它们。为UIViewAlertForUnsatisfiableConstraints设置一个象征断点,确保测试时捕获不明显的错误。
3.2.2 阻止不可满足的约束
不可满足约束相对容易修复。当不可满足约束产生时,系统会通知你,并提供冲突约束的列表。
当你知道错误后,解决方案通常会很简单。移除其中一个约束,或者修改为可选约束。
但是,有一些常见问题值得进一步检查:
- 不可满足的约束通常发生在通过代码添加视图到视图层级结构时。
默认情况下,新视图设置translatesAutoresizingMaskIntoConstraints属性为YES。当在画布中开始添加约束到视图时,界面生成器自动设置该属性为NO。但是,当你通过代码创建和布局视图时,在添加自定义约束前,需要设置属性为NO。 - 不可满足的约束通常发生在视图层级结构在一个很小的空间内显示时。
通常,你会预料到视图访问的最小空间,并且正确设计你的布局。然而,国际化和动态类型会导致视图的内容比期望的更大。当可能的排列数量增加时,越来越难保证你的布局会在所有情况下正常工作。
相反,你可能希望构建一个故障点(failure point),让你的布局在一个可预料的,可控的方式失败。 - 考虑改变一些必需约束为高优先级的可选约束。当发生冲突时,这些约束让你控制打破布局的什么地方。
例如,指定故障点的优先级为999。在大部分情况下,这些高优先级约束作为必需的约束。但是,当发生冲突时,高优先级的约束被打破,保护剩下的布局。
类似的, 避免给有固有内容尺寸的视图指定必需的content-hugging或者compression-resistance优先级。通常,一个控件的尺寸作为理想的故障点。控件大一点或者小一点对布局没有任何有意义的影响。
没错,有些控件应该只在它们的固有内容尺寸显示;但是,这种情况下,控件有一点偏移量也比以不可预料的方式打破布局更好。
3.3 有歧义的布局
有歧义的布局发生在系统的约束有两个或多个解时。有两个主要原因:
- 布局需要额外的约束唯一指定每个视图的位置。
检测到哪个视图有歧义后,添加约束唯一指定视图的位置和尺寸。 - 布局有相同优先级的可选约束的冲突,并且系统不知道应该打破哪个约束。
此时,通过改变优先级,让它们不相等,告诉系统应该打破哪个约束。系统首先打破优先级低的约束。
3.3.1 检测有歧义的布局
与不可满足的布局一样,界面生成器通常可以在设计时检测和提供建议,来修复有歧义的布局。这些歧义显示在问题导航器的警告,文档大纲的错误和画布中的红色线条。更多信息请参考“识别不可满足的约束”。
与不可满足的布局一样,界面生成器不能检测到所有可能的歧义。许多错误只能通过测试发现。
当运行时发生有歧义的布局时,自动布局选择使用一个可能的解。这意味着布局可能或者不可能如你期望的那样显示。此外,控制台没有警告,不能为有歧义的布局设置断点。
因此,有歧义的布局比不可满足的布局更难检测和识别。即使歧义有很明显的可视化影响,也很难检测错误是因为歧义还是布局的逻辑错误。
幸运的是,你可以调用一些方法来帮助识别有歧义的布局。所有这些方法只在调试中使用。在能访问视图层级结构的地方设置断点,然后在控制到调用以下其中一个方法:
- hasAmbiguousLayout。同时对iOS和OS X有效。在一个错位的视图调用该方法。如果视图的frame有歧义,该方法返回YES。否则返回NO。
- exerciseAmbiguityInLayout。同时对iOS和OS X有效。在有歧义布局的视图中调用该方法。这会在系统的可能的有效解中切换。
- constraintsAffectingLayoutForAxis:。对iOS中有效。在视图中调用该方法。该方法返回指定坐标轴上,影响该视图的所有约束的数组。
- constraintsAffectingLayoutForOrientation:。对OS X有效。在视图中调用该方法。该方法返回指定方向上,影响该视图的所有约束的数组。
- _autolayoutTrace。在iOS上作为一个私有方法有效。是视图中调用该方法。该方法返回包括该视图的整个视图层级结构的诊断信息字符串。有歧义的视图被标记,因此设置translatesAutoresizingMaskIntoConstraints为YES。
在控制台运行这些命令时,可能需要使用Objective-C语法。例如,断点暂停执行后,在控制台窗口输入call [self.myView exerciseAmbiguityInLayout],调用myView对象的exerciseAmbiguityInLayout方法。类似的,输入po [self.myView autolayoutTrace],打印包括myView的视图层级结构的诊断信息。
提示
运行以上诊断方法之前,确保修复了界面生成器找到的所有问题。界面生成器试图修复它找到的任何错误。这意味着,如果它找到一个有歧义的布局,它会添加约束,让布局不再有歧义。
因此hasAmbiguousLayout会返回NO。exerciseAmbiguityInLayout没有任何效果,constraintsAffectingLayoutForAxis:可能返回额外的,出乎意料的约束。
3.4 逻辑错误
逻辑错误是简单的bug。某些地方,可能与错误有关。它可能与自动布局如何计算视图的frame有关。它可能与你创建的约束集,或者与你设置的视图属性有关。它可能与约束如何与创建复杂行为的交互有关。不管怎么样,有些事情有些地方不匹配你的心理模型。
逻辑错误是最难查找的。排除所有其它可能性后,剩下的即使再不可能,也肯定是逻辑错误。但是,即使已经检测到有一个bug,你仍然需要找到错误发生在哪里。
这里没有工具或者步骤介绍。修复逻辑错误通常涉及实验和迭代测试,用来识别问题和指出如何修复。但是,这里有一些建议可能会帮到你:
- 检测现有的约束。确保你没有缺少任何约束,或者不小心添加了不想要的约束。确保所有约束都连接到正确的项和属性。
- 复查视图的frame。确保没有意外拉伸或者缩小。
对没有可见背景的视图尤其重要,比如标签或按钮。这些项被意外的调整大小时,不会很明显。
一个重新调整大小的症状是基线对齐的视图没有正确排列。这是因为只有视图在固有内容高度显示时,基线对齐才会正常工作。如果垂直拉伸或缩小视图,文本会神秘的出现在错误的位置。 - 如果控件总是匹配它的固有内容尺寸,设置一个很高的content-hugging和compression-resistance优先级(例如999)。
- 查找所有你对布局做的假设,并添加明确的约束,确保这些假设为真。
记住,不可满足的布局通常是最容易查找和修复的问题。添加额外的约束,直到发生冲突,然后检查并修复冲突。 - 尝试理解给定的约束为什么会产生你看的结果。如果你理解了,就能很好的修复它。
- 尝试使用代替的约束。通常,一个问题自动布局会给你很多解决方法。尝试不同的方法可能会修复问题,至少更容易找到错误。
3.5 调试技巧和窍门
以下主题描述收集和组织布局信息的技术,也描述你可能遇到的意外行为。你可能不需要再每一个布局上使用这些技术,但它们可以帮助你解决最难的问题。
3.5.1 理解日志
视图的信息被打印在控制台,不管因为有一个不可满足的布局,或者因为你使用constraintsAffectingLayoutForAxis:或constraintsAffectingLayoutForOrientation:调试方法显式打印约束。
不管哪里方法,你都能在这些日志中找到很多有用的信息。这是一个不可满足布局的错误输出:
2015-08-26 14:27:54.790 Auto Layout Cookbook[10040:1906606] 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)
(
"=400)]>",
"",
"",
"",
""
)
Will attempt to recover by breaking constraint
=400)]>
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in may also be helpful.
这些错误信息显示了5个冲突约束。同一时间,这些约束不全为真。你需要移除一个,或者转为可选的约束。
幸运的是,视图层级结构相对简单。有一个父视图包括一个标签和一个文本框。冲突约束设置了以下关系:
- The label’s width is greater than or equal to 400 points.
- The label’s leading edge is equal to the superview’s leading margin.
- There is an 8-point space between the label and the text field.
- The text field’s trailing edge is equal to the superview’s trailing margin.
- The superview’s width is set to 320 points.
系统尝试通过打破标签的宽度来恢复。
提示
约束使用Visual Format Language写入控制台。即使你从来没有使用过Visual Format Language创建自己的约束,也必须可以阅读和理解它,来有效的调试自动布局问题。更多信息请参考“Visual Format Language”。
这些约束中的最后一个由系统创建,你不能修改它。此外,它创建了一个与第一个约束明显的冲突。如果父视图只有320个点宽,你就不能使用400个点宽的标签。幸运的是,你不需要去掉第一个约束。如果把它的优先级降为999,系统仍然尝试提供选择的宽度——尽可能接近,同时仍然满足其它约束。
基于视图的autoresizing mask(例如,当translatesAutoresizingMaskIntoConstraints为YES时创建的约束)的约束有额外的mask信息。放置约束后,日志字符串显示h=,后面跟着三个字符,和v=,后面跟着三个字符。-(连字符)表示一个固定值,&表示可变值。对于水平mask(h=),三个字符表示左边页边留白,宽度和右边页边留白。对于垂直mask(v=),它们表示顶部页边留白,高度和底部页边留白。
例如,考虑以下日志信息:
"
该信息由以下几部分组成:
- NSAutoresizingMaskLayoutConstraint:0x7ff28252e480:约束类和地址。这个例子中,类告诉你它基于视图的autoresizing mask。
- h=--& v=—&:视图的autoresizing mask。这是默认的mask。水平方向上,有固定的左边页边留白,固定的宽度和可变的右边留白。垂直方向上,有固定的顶部页边留白,固定的高度和可变的底部页边留白。换句话说,当父视图的尺寸改变时,视图的左上角和尺寸仍然是常量。
- H:[UIView:0x7ff282617cc0(50)]:Visual Format Language描述的约束。这个例子中,它定义了50个点宽度的单个视图。该描述还包括了约束影响的任何视图的类和地址。
3.5.2 为日志添加标识符
尽管上一个例子理解起来相对简单,但更长的约束列表变得很难理解。你可以为每个视图和约束提供一个有意义的标识符,让日志更容易阅读。
如果视图有明显的文本组件,Xcode用它作为标识符。例如,Xcode使用标签的文本,按钮的标题,或者文本框的占位符来标记这些视图。否则的话,在标记检查器(Identity inspector)中设置视图的Xcode具体标签。界面生成器在它的界面中使用这些标识符。许多标识符也在控制台日志中显示。
对于约束,通过代码或者属性检查器设置它们的identifier属性。当在控制台打印信息时,自动布局使用这些标识符。
例如,这里是同一个,设置了标识符的不可满足的约束错误:
2015-08-26 14:29:32.870 Auto Layout Cookbook[10208:1918826] 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)
(
"",
"=400)]>",
"",
"",
""
)
Will attempt to recover by breaking constraint
=400)]>
Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in may also be helpful.
正如你所看到的,这些标识符让你可以快速的,容易的在日志中识别出约束。
3.5.3 可视化视图和约束
Xcode提供了工具,帮助你在视图层级结构中可视化视图和约束。
要在模拟器中查看这些视图:
- 在模拟器中运行应用程序。
- 切换回Xcode。
- 选择Debug > View Debugging > Show Alignment Rectangles。该设置画出视图边缘(edges)的轮廓。
对齐的矩形框是自动布局使用的边缘。打开这个选项,可以快速认出所有意外尺寸的对齐矩形。
如果你需要更多信息,在Xcode调试栏中点击Debug View Hierarchy()。然后Xcode显示一个可交互的视图调试器,提供一些工具用于探索和与视图层级结构交互。调试自动布局问题时,“Show clipped content”和“Show constraints”选项尤其有用。
启用”Show clipped content“选项,会显示不小心放到屏幕外的视图的位置。启用”Show constraint“选项会显示影响当前选中视图的所有约束。当事情开始变得古怪时,两个选项提供了快速明智的检查。
更多信息请参考”Debug Area Help“中的”Debugging Views“。
3.5.4 理解边缘情况
这里是一些边缘情况,导致自动布局以意外的方式表现:
- 自动布局根据视图的对齐矩形(alignment rectangles)放置视图,而不是它们的frame。大多数时候,这些值是相等的。但是,有些视图可能会设置自定义对齐矩形,从布局计算中排除视图的一部分(例如badges)。
更多信息请参考”UIView Class Reference”中的“Aligning Views with Auto Layout”。 - 在iOS中,可以使用视图的transform属性调整大小,旋转,或者移动视图;但是,这些变换不会以任何方式影响自动布局的计算。自动布局根据视图未转换的frame计算视图的对齐矩形。
- 视图可能在它的bounds之外显示内容。大多数时候,视图的行为是正确的,限制内容在bounds中。但是因为性能的原因,这不会被图形引擎强制执行。这意味着视图(尤其是自定义绘制的视图)可能在跟frames不同的尺寸中绘制。
你可以通过设置视图的clipsToBounds属性为YES,或者检查视图的frame尺寸来识别这些bug。 - 只有当所有视图在它们的固有内容高度显示时,NSLayoutAttributeBaseline,NSLayoutAttributeFirstBaseline和NSLayoutAttributeLastBaseline属性才会正确对齐文本。如果其中一个视图垂直拉伸或者压缩,它的文本可能出现在错误的位置。
- 约束优先级在整个视图层级结构中是一个全局属性。通常可以通过在堆栈视图,布局向导或者虚拟视图中分组视图,来简化布局;但是该方法不会封装视图的优先级。自动布局继续比较分组内核分组外(甚至其它分组中的优先级)的优先级。
- 长宽度约束允许水平和垂直约束相互影响。正常情况下,分别计算水平和垂直布局。但,如果约束视图的高度相对于它的宽度,就会在垂直和水平约束之间创建间隙:它们可以相互影响和冲突。这些影响极大的增加了布局的复杂性,并导致布局的不相关部分之间发生意外的冲突。