开发者会经常遇到视图或者Auto Layout约束中存在bug的情况,并且这种bug很难通过代码发现,所以开发者很有必要熟知如何进行简单高效的视图调试,而Xcode 6的发布使得视图调试变得前所未有的简单。
开发者不用将frames打印到控制台,然后在脑海中可视化视图的布局,现在你可以在Xcode中查看整个视图层次。
本教程会带你熟悉所有可以操作的不同选项。你做好写代码的准备了吗?这个问题有点令人烦心,因为你根本就不想写代码。你可以在Xcode 6中检查开源库的视图层次,了解它的编写方法---最重要的是不用看任何代码。
开始
本教程使用Jesse Squires 编写的JSQMessagesViewController库,库的UI看起来非常熟悉,类似Messages app。
先在 GitHub project page下载源码并解压。
注意: 该库使用了CocoaPods来管理和其他库之间的依赖关系,不熟悉的话可以先看看CocoaPods tutorial。
接着在终端找到解压项目,运行 pod install 安装所需依赖关系。打开JSQMessages.xcworkspace并编译,然后在iPhone5s模拟器中运行应用程序。 (可使用任意尺寸的模拟器,本教程中使用的是4英寸的模拟器,你可以选择4英寸模拟器以便于快速了解教程内容)
注意: Live View Debugging仅支持在iOS 8上运行应用程序,不支持iOS 7,即便你用的是Xcode 6。
点击项目中的 Push via storyboard选项 进入与Steve Jobs 和Tim Cook消息发送界面线程,这就是你将要查看的视图。
Even Steve Wozniak joins the fun!
回到Xcode并点击调试栏中的Debug View Hierarchy按钮 ,或者通过Debug\View Debugging\Capture View Hierarchy操作,效果是一样的。
Xcode会打断app的运行并进行调试,该操作和你使用调试栏上的的"pause"按钮暂停app运行一样。此外,Xcode会展示canvas(译者注:以下简称"画布")而不是代码编辑器。Xcode在canvas上绘制了app主窗口的整个视图层次,包括指示每个视图边界的细线(称之为线框图)。
如果往视图层次上添加一个子视图,也就是在当前的视图堆栈上添加layer。由于大部分views不会叠加,所以运行app时,所有的views看起来像是大layer的一部分,下图非常接近这种描述,不过带着一些额外的线。
现在你所看到的是一个可视化的视图堆栈。在canvas中点击并拖动,会看到视图层次的3D模型。
你可以从上、下、左、右多个角度查看视图层次。
注意:在尝试的过程中,canvas可能不会像教程中展示的这样。为确保你是在同一个页面,请按下cmd + 6 调出Debug navigator。
在面板底部左侧有两个按钮。如下图所示,取消对这两个按钮的选定,否则会隐藏一些视图。
探索视图层次(Exploring the View Hierarchy)
最自然最常用的方式是从左边开始探究3D模型,稍后教程为解释为什么要这么做。向左拉动视图层次,如下图所示:
如果你想可视化app的构建,那么从多个角度查看视图层次就非常有用了。不过,在堆栈的底部(左边)有很多空视图,它们是什么呢?
点击最左边的视图(也就是最后边的视图),Xcode会对其进行高亮。画布上方的Jump Bar(跳转栏)更新展示一个UIWindow作为最新项目--最新项目通常指出了当前选中的项目以及其class类型。
由于该app只使用一个窗口,所以可假定位于跳转栏前边的UIWindow 是app的主要窗口,也就是AppDelegate的 window 属性。
不过,似乎检查这个图层并没有多大意义。那下一个视图呢?在画布中,点击窗口最右边的视图(也就是最上边的视图),再看看跳转栏(Jump Bar)看看有什么不一样的。UILayoutContainerView,甚至不是一个公开类。
这时候,视图层次看起来是这样的:
1.UINavigationTransitionView:导航控制器在这里发生转场行为的容器视图。
2.UIViewControllerWrapperView: 包含view controller 的view属性的封装视图。
3.UIView: view controller的最上层视图 (与view controller的view 属性一致)
4.JSQMessagesCollectionView: 工程使用collection view来展示所有消息。
Focusing on Views of Interest(关注与调试相关的视图)
在调试该特定视图层次时,头四个视图(从窗口开始)实际上是视觉"噪音",它们并无多大意义,会让你分心,如果能过滤掉这些"噪音"视图就最好不过了。
你当然可以这么做!在画布的右下角有一个双滑块儿滑杆,左右滑动滑块可帮你暂时隐藏一些视图,默认情况下,滑块在滑杆的左右两端。
将滑块左端滑块向右滑动一点,画布中app的线框图会一层层消失。将滑块儿拖得更远一点,UINavigationTransitionView也消失不见了。
根据需要将左端滑块儿尽可能拖得远一些,从而隐藏JSQMessagesCollectionView的父视图。你的画布看起来应该和下边类似:
在右侧,你会发现导航栏似乎有点让人分心,但是它的的确确位于collection view的最上层,不方便查看下层布局。幸运的是你可以隐藏它。
由于你主要关于屏幕上较小的一个区域,并且导航栏上还有很多比导航栏更小的元素。这时候放大导航栏可以让你更清楚地看到界面是如何布局的。
使用缩放控件按钮,它是一组三个的按钮,居中展示在画布中。
这一组按钮有放大"+"、缩小"-"以及将视图重置到正常的水平的"="三种选择,
注意:如果你使用的是触控板,两指捏合和缩放也能缩小和放大视图。如果你一次性缩放过多,那么屏幕上的内容就不能完全展示,这时候使用触控板还是比较有用的。当然你也可以使用鼠标滑轮进行缩放。
虽然通过缩放toolbar获得额外的细节非常不错,但这些视图仍紧紧地叠加在一起,要分清谁是谁并不容易。
想要解决这个问题,可以使用画布左下角的spacing slider,向右拖动圆形滑块儿越远,不同同视图之间的间距。视图间的间距会随着滑块儿拖动的距离增加而变大。
在该案例中,尽可能地向右移动滑杆儿,以避免工具栏中视图叠加。你可以试试在画布上拖放视图以达到理想的效果。
画布右下角隐藏视图的滑杆,将右端滑块儿移至左端,直到剩下UINavigationBar。选择最上边的图层,你可以使用Jump Bar来辨认每个视图的类。首先你会看到导航项目消失了,接着是包含它们的按钮,然后是一些私有视图,最后是导航栏。
啊?没有导航栏了!
注意:旋转画布查看3D视图层次,如果最顶层视图位于左侧,那么滑杆儿的左侧滑块儿依旧从堆栈底部移除视图,现在是在右侧。同样,不管顶层视图在左侧还是右侧,右侧滑块儿都从堆栈的顶部开始移除。
将滑块儿从左侧移至右侧,视图从右向左逐渐消失(反之亦然),这是有违直觉的,这也是让顶层视图位居右侧,这种查看3D模型的方式才是最自然的方式,就是我们在教程最初提到的那一点。
不幸的是,隐藏导航栏(包含_UIBackdropView根视图)视图也会让屏幕底部toolbar里的内容消失。调整缩放度或向下移动画布才能看到发生的变化。
由于toolbar项目是屏幕上重要的一部分,你需要查看这些内容,所以仅仅隐藏视图直到(但不包括)剩下_UIBackdropView,导航栏堆栈看起来像是下边这种:
More View Options(更多视图选项)
现在已经隐藏了不相关的视图,我们接着要从正面再看这个屏幕。你可以将3D模型拖回原位,不过有时候很难刚刚好,还好我们有其他办法。
在3D模型的下方---缩放按钮的左侧有一组四个按钮,从左向右数第三个按钮是ResetViewing Area按钮,它可以取消旋转并从正面展示视图层次,像在模拟器或者设备上一样。
画布看起来应该和下边的一样:
你可能注意到你在调试器中所见到的,并不全是app实际运行时的样子。
首先,单个视图周围易燃有线框图包围,它们可以让你查看透视图或者没有任何内容的视图,不过如果你不需要细节信息,线框图就会让事情变得杂乱。
你可以使用View Mode按钮进行选择-在Reset Viewing Area按钮的右侧。点击视图模式按钮,你可以选择只查看线框图、只查看内容或者同时查看两者。
如果你主要是对位置感兴趣,并且不大关心视图看起来什么样子,那么线框图是非常有用的。当你想要调试视图的外观时,仅展示视图内容就非常有用了。
想要减少线框图引起的杂乱(尤其是靠近导航栏和toolbar的地方),可将查看模式更改为Contents ,以移除所有线框图,仅留下app的核心部分。
接下来,是从当前视图中忽略的几点。
当你运行app时,你将会看到文本气泡上的标签,指示信息发送者名称或者信息的时间戳,以及最后一个气泡中Golden Gate Bridge的图片。但是调试器并不会展示这些标签和图片。想要解决这个问题,可查看画布上中间一组按钮的第一个,可展示或者隐藏省略掉的视图。这些视图的clipsToBounds属性设置为了YES。
这是标签相关的,大概由于较长的名字和日期不应当延伸到标签的界限之外。这一点同样应用于图片,图片使用圆角半径并剪切以生成圆角图片。点击该按钮,你会注意到这些视图将不再出现在Xcode中。
注意:你可能注意到可视项目周围仍有线框图,如果是这种情况,使用你先前使用的View Mode按钮
打开和关闭线框图,问题即可解决。
你已经都了解了:在Xcode中近乎完美地复制了视图层次。
So easy!
Inspecting Views(查看视图)
已经了解了最重要的部分,现在看看这些不同视图的布局。
你已经知道了collection view如何让这些视图聚集在一起,但是如果能看到这些不同元素的整体结构就更好了。当然可以啦!
按下cmd + 6 调出Debug navigator,和其他调试会话一样,Debug navigator提供了当前会话的文本信息。对于视图调试来说,这意味着Xcode提供了所有窗口中视图的视图树。展开Debug navigator的视图树后是这个样子的:
注意:在Debug navigator的底部,你会看到一个选项可以控制在视图树中展示哪种类型的项目。苹果的文档表示左边的按钮将系统视图实现的私有元素过滤出来,不过这个按钮在Xcode 6.2中似乎不起作用。
右边按钮可隐藏那些将其 hidden 属性设置为YES的视图,并且搜索栏仅展示匹配搜索条件的视图和约束。
出于本教程的目的,取消对这两个按钮的选择,并且不使用任何搜索过滤。
这是个不错的而开始。展开最后一个JSQMessagesCollectionViewCellOutgoing,它只有一个子视图UIView。如果你以前使用过collection view,那你应该知道这个是讲得通的,因为任何UICollectionViewCell都有一个包含cell内容的contentView 属性。
点击但不要展开-将鼠标放在Debug navigator的UIView 上,你会看到Xcode已经在画布上对其高亮,这样你就准确知道它在屏幕的什么地方。
想要真正了解iOS如何放置该cell,可使用cmd + option + 4打开Size Inspector,该导航器的顶部形象化了视图的边界、位置以及锚点。
不过,真正有趣的部分是应用于该视图的Auto Layout约束列表。你可以立刻将cell的内容视图的宽和高分别设定为312 point和170 point,并将其居中。封闭的cell同样是312*170 point,所以内容视图占据了整个cell。下边用灰色显示的约束表示它们是指出视图和其子视图之间关系的约束。
想了解一个特定约束的更多细节,首先要展开视图树中的视图,然后展开Constraints项目。你将会看到和Size navigator中一样的约束列表。
点击第一个约束(self.midX的约束),并通过cmd + option + 3切换至Object inspector。你会看到一个约束概览。编辑约束时,这一点非常类似于Interface Builder。
除了尺寸和约束信息,你还会看到Object Inspector中特定视图的其他信息。回到Debug navigator,展开视图树中的UIView ,你会到它有3个JSQMessageLabel和两个UIView。选中第一个JSQMessageLabel(带有时间戳的那个),并打开Object Inspector。
第一个部分展示了对象的类名称和内存地址,第二部分展示了对象的多个公有属性的值。
从图中看出,标签文本的颜色是无alpha 的0.67灰,字体大小是12pt。
针对它们如何被可视化,其他类也有一些有用的信息。回到Debug navigator,展开cell的根UIView 中的第二个UIView,你会看到一个UIImageView。
从视图树中选择image view,并查看Object inspector。
你正查看的是展示用户头像的视图-在该例子中是作者的首字母JSQ,你会看到常规图片、便利的贴有标签的图片、较暗的图片以及被标记的高亮,这些将在用户点击cell时展示。
在cell的根视图中,JSQMessageLabel的其他两个实例当前还没有文本,但它们被用于即将进来的消息发送者的名字和消息发送失败时的错误信息。
怎么样,在Xcode中调试视图非常简单吧,继续运行该app,点击Debug bar上的"Continue"按钮,或者执行Debug\Continue,就像你在常规调试中那样。
Live Modifications(实时调整)
现在你已经了解了使用Xcode 6进行视图调试的基础支持,接下来将你所学应用到一个小小的实验中:只使用调试器,确保你在本教程中使用的collection view的垂直滚动指示器为red。
你可以从以下两点开始:
1.由于视图调试和其他调试部分非常像,你可以在终端使用expr 和其他命令,但是需要重新运行项目才能看出所做的变化。更多关于这些命令的信息,请查看debugging apps in Xcode教程。
2.由于Objective-C中的指针仅仅是内存地址,所以当你发送对象时,你仅仅发送了一个内存地址。这同样适合于调试器,所以类似po 0x123456abcdef这样的命令会打印出内存地址中的对象描述。
多做几次尝试,如果出现问题,可尝试以下解决方案:
首先要确保将视图模式设置为"Contents"
在Debug navigator中,展开collection view的视图树,以便清楚知道视图的子视图。
collection view的最后视图是两个UIImageView实例,是水平方向和垂直方向上的滚动指示器。点击第二个,你会看到水平方向上的指示器在画布中被高亮。
从Object inspector中复制图片视图的内存地址。
在这个例子中,滚动指示器的内存地址是0x7fde6c484640。需要做的是给该地址中的对象发送一个setBackgroundColor:信息可将滚动指示器着色。可使用以下代码:
expr (void)[0x7fde6c484640 setBackgroundColor:[UIColor redColor]]
继续运行该应用程序,在滚动collection view时,滚动指示器变成了红色。
祝贺你,你已经了解了使用Xcode 6进行视图调试的基本内容。
Old School Debugging(保守调试)
实时调试让使用Xcode 6进行视图调试变得非常简单,但并不意味你之前常用的调试方法已经没有用武之地了。事实上,除了视图调试外,iOS 8引入了一个非常受欢迎的技巧: _printHierarchy.
注意:你已经了解了Xcode 6视图调试的基本内容,所以如果你不喜欢可以直接跳过这个章节。不过如果你非常痴迷一些便捷的旧的技术,那不要错过这个。
打印View Controller Hierarchy
_printHierarchy是 UIViewController 的一个私有方法,你可以用它将view controller 层次打印到控制台。编译并运行,选中Push via storyboard,然后点击Debug bar上的"pause"按钮。
在终端打出以下内容并重新运行:
po [[[[UIApplication sharedApplication] keyWindow] rootViewController] _printHierarchy]
这告诉你UINavigationController的第一个视图控制器是一个TableViewController,你可以选择如何推出控制器。第二个view controller是DemoMessagesViewController,或者你已经调试过的view controller。
这个例子似乎不怎么令人兴奋,但如果你的导航控制器中有几个view controller,或者弹出视图中有tab bar控制器,那么想要弄清楚这些view controller如何工作, 这个功能就非常有用了。
Printing the View Hierarchy(打印视图层次)
如果你更喜欢文本化的视图层次,那你可以使用UIView旧的私有recursiveDescription。这个方法打印出来的视图层次非常类似于上边描述的view controller层次。
打开Views\JSQMessagesCollectionViewCellOutgoing.m 并在awakeFromNib中添加一个断点。
编译并运行,然后选择Push via Storyboard。调试器有问题了,因为加载了JSQMessagesCollectionViewCellOutgoing。现在在控制台输入以下代码:
po [self.contentView recursiveDescription]
这将打印出 JSQMessagesCollectionViewCellOutgoing的contentView层次, 看起来像这样:
这是基本的,但可以帮你调试iOS 8之前的视图层次。
Using debugQuickLookObject(使用debugQuickLookObject)
最后,Xcode5.1引入了Debug Quick Look功能。如果你已经做好了调试的准备,但不大想知道某段代码如何实现对象的特定外观,那么这时候这个功能就非常有用了。
你的自定义类可以实现debugQuickLookObject方法,并返回任何由Xcode展示的内容。此外,如果你已经正进行调试,并且已经有了想要查看的对象,你可以使用快速查看功能,并且Xcode会以可视化形式表示对象。
比如,NSURL对 debugQuickLookObject的实现返回了一个带有URL 的UIWebView,你可以真实看到URL背后的东西。
关于使用Quick Look调试的更多信息,请查看相关文档。
下一步
以上是关于实时调试的内容,它是一个可以帮助节省大量时间的工具,非常好用。