可视化扩展布局

我注意到许多在线用户(即StackOverflow)在视图布局不正确时会跳到少数UIViewController属性,然后几乎随机地操作它们以获得所需的行为。 这总是导致对布局问题的“可接受”答案是“自动将AdjutstsScrollViewInsets设置为[true / false]”或“将edgesForExtendedLayout设置为UIRectEdgeZero”,其中一个人说“谢谢,已修复!”,另外五个人说“那没错”。修复它会有所帮助”。 即使在Apple的WWDC视频中,也没有关于这些API的工作原理以及它们相互之间有什么影响的超级清晰的解释,尽管WWDC 2013在发布时有广泛的概述。 我找不到关于其所有工作原理的出色文章,所以我想针对这些API的工作原理发布PSA,以便我们可以开始以正确的方式修复问题并停止永久保存不良信息。

我认为具有交互属性的示例应用程序绝对是了解UIKit来龙去脉的最佳方法。 如果你玩的够多的话,你就可以开始预测行为。 一旦您可以解释更改给定属性将对结果布局产生什么影响,便有了相当不错的掌握。 因此,为了说明这一切是如何工作的,我将包括一个示例应用程序的屏幕截图,然后为您提供一个停顿的地方,并思考改变给定属性的效果。 当然,您可以浏览一下,但是我想您会发现尝试提前计算出最终的布局会更有优势。

为此,我们将从iOS 10开始,它使我们可以忽略新引入的safeArea及其带来的其他API(和注意事项)的影响。 我们将在不久的将来覆盖这些内容。


这是所有具有实例化时视图/视图控制器具有的默认值以及在UINavigationController内部显示tableView时的默认外观的所有内容。 请注意,tableView的背景为绿色,但单元格背景为红色。 我们可以使用它来查看tableView的开始位置及其内容的开始位置; 当这两个不同时,我们正在查看一个非零contentInset,它显示在底部的单元格中。

这个contentInset来自哪里? 如果您检查代码,我们不会自己指定。 自动输入AdjustsScrollViewInsets


UIViewController属性autoAdjustsScrollViewInsets是iOS 7(对界面进行了大修的版本)中引入的,默认为true。 在iOS 7中,导航栏默认为半透明,并且为了展示半透明性,视图必须在导航栏下方开始(如果没有内容可看,您将无法欣赏半透明!)。 但是,如果视图从导航栏后面开始,则很明显,如果没有一些Apple Magic™来为开发人员处理事情,事情就会变得模糊。 这种魔术以automaticAdjustsScrollViewInsets的形式出现,它进入UIViewController的子视图并运行此测试:

  func magicView()-> UIView?  { 
保护self.isKind(of:UIScrollView.self)else {
返回self.subviews.first?.shouldApplyMagic()
}
返回自我
}

并自动设置该滚动视图的contentInset以容纳navigationBar。


ContentInsets是非常基本的,我仅将这一部分包括在内,以快速提醒那些尚不清楚的人。 我发现,如果您将UIScrollViews视为内容的“窗口”,则更容易理解它们在做什么; 窗口框架本身不会改变大小,但是您可以四处走动以查看窗口框架后面的其他内容。 您所看到的不止是那里,所以一旦您触及内容的最上/最下部分,就无法再进行任何操作(忽略弹跳的UIKit给我们提供的内容)。 使用相同的隐喻,contentInset本质上将“空白”内容添加到您正在查看的内容上方,这意味着您可以看到比其他地方更多的内容。 如果我将topContentInset添加为100点,则意味着我可以滚动到内容的顶部,然后再在其上方滚动100点。 对于UITableView的基本情况,这只是空白,它将显示您的backgroundColor是什么,但是UIScrollViews可以让您做更多的事情。 我现在不会讲这个; 也许解释UIScrollViews是另一天的好主意。

既然我们对automaticAdjustsScrollViewInsets有了一个不错的了解,那么应用程序的初始状态就应该有意义。 我们的视图从UINavigationController的顶部开始(当然,它的视图)。 默认情况下,Apple会自动为我们提供AdjustsScrollViewInsets == true,因此,在布局视图时,它会看到导航栏的高度为64(进入状态栏区域),并在tableView上方为我们提供了64个空白内容。 实际上,这会在视觉上将tableView向下推,现在可以让我们在需要时在工具栏后面滚动,但我们不必处理最初停留在导航栏后面的内容的默认状态。


愚蠢的答案是,edgeForExtendedLayout是人们设置为.zero(或Objective-C中的UIRectEdgeNone)的属性,当他们无法弄清楚为什么视图不正确偏移而又不真正了解发生了什么情况时。

更准确地解释,“扩展布局”是Apple在iOS 7中引入的行为的名称以及默认的半透明导航栏。 视图的扩展布局是指视图中可以“拉伸”的部分。 默认情况下,edgesForExtendedLayout包含所有边缘。 考虑到iOS 7中的默认半透明性,这应该是有道理的。默认情况下,Apple将所有导航栏设置为半透明。 如果我们的视图没有“开始”在导航栏后面,则无法利用它。 对于tableViews,这是通过将视图固定到navigationController视图的顶部,然后在表视图上设置适当的contentInset来实现的。 小测验时间!

要重复,默认情况下,我们看起来像这样:

导航栏是半透明的,edgesForExtendedLayout包含所有边缘(顶部是此处的相关边缘),并且我们会自动调整AdjustScrollView插图。 结果,我们可以在导航栏后面看到,我们的视图延伸到导航栏的顶部,并且tableView的contentInset设置为导航栏的高度。

如果我们自动关闭AdjustsScrollViewInsets,会发生什么? 提示危险! 音乐。

我们的导航栏是半透明的,我们的edgeForExtendedLayout仍然包括顶部边缘,但是我们的视图控制器不再寻找其子视图来寻找用于设置contentInset的UIScrollView。 结果,我们可以在导航栏后面看到,我们的表格视图从导航控制器的顶部开始,但是内容没有插入,因此我们被一个模糊的视图所卡住。

如果我们转弯不允许顶部边缘成为扩展布局的一部分怎么办?

即使导航栏是半透明的,也不允许我们的上边缘参与扩展的布局。 结果,我们的视图现在固定在导航栏下方 。 您可以看到,即使滚动,也看不到导航栏后面的单元格。 借用了较早的隐喻,“窗口框架”不再位于导航栏的后面。 我们的contentInset设置为零,因为我们不再需要在导航栏后面开始。


我不会为此提供图片,因为我们拥有所有需要解决的上下文。 假设我们再次从默认状态开始:automaticAdjustsScrollViewInsets无效,因为我们的视图将不再从导航栏后面开始。 将我们的上边缘包含在edgeForExtendedLayout中也不会有任何效果,因为如果我们有半透明的导航栏,则没有扩展的布局。 无论您使用这两个属性进行了什么更改,如果我们的导航栏是不透明/不透明的,我们的视图将在导航栏下方开始,并且contentInset永远不会改变。


因此,我们已经确定,extendedLayout是Apple默认情况下将视图移到半透明导航栏之后的方法。 在上一节中,我们发现禁用了半透明导航栏后,由于没有半透明性,因此扩展布局不再在顶部边缘起作用。 但是,UIViewController的extendedLayoutIncludesOpaqureBars更改了该逻辑,并且该名称现在应该很直观:如果将该属性设置为true,则即使导航栏不是半透明的,扩展布局逻辑也会起作用。

从默认状态开始, 但是具有不透明/不透明的导航栏,如果现在告诉UIKit在确定如何根据扩展布局布局视图时希望它包括不透明栏,应该怎么办? 再次点击音乐…

这有点棘手。 请记住,automaticAdjustsScrollViewInsets默认为true,所以无论扩展布局发生什么,UIKit都会更改我们的contentInset以确保我们的内容从视觉上在导航栏下方开始。 现在,当我们将其关闭时会发生什么? 回想一下,我们实质上是在告诉UIKit假装我们的导航栏是半透明的……

是的,我们的视图现在停留在不透明的导航栏后面,因为不再为我们更改contentInset。 如果我们不再在扩展版式中包括顶部边缘,会发生什么?

一切恢复正常,我们的表格视图位于导航栏的下方,而不是后面。


只是为了解决根本不包含导航栏(半透明或其他方式)的情况,唯一有效的属性是autoAdjustsScrollViewInsets(它确定是否在状态栏下方偏移)和edgeforExtendedLayout(它确定是否)您的视图是否在状态栏后面延伸。 如果您的视图未触及状态栏,则这两个属性均无效。


我们应该对我们的UITableViews和UIScrollViews在iOS 10中具有扩展布局时的表现相对有信心。非UIScrollViews呢? 库存UIViewControllers(当然还有库存UIViews)在扩展布局的世界中表现如何? 为此,我要删除“ automaticallyAdjustsScrollViewInsets”配置。 我们已经知道该属性将使用上面的magicView()方法来找到要操作的滚动视图,但是这里没有。

无论如何,在这个没有UIScrollView的新世界中,可能会发生一些不同的事情。 如果将子视图固定到topAnchor,则会得到以下信息:

如果将其固定到视图控制器的topLayoutGuide,则会得到以下信息:

这应该可以澄清topLayoutGuide(如果我们之前使用的是工具栏,则为bottomLayoutGuide)在以前不太清楚的情况下会做什么。

这时属性有什么作用?

显示/隐藏导航栏只会更改topLayoutGuide所在的位置; 它只会覆盖状态栏:

如果我不使用UIScrollView,那么edgeForExtendedLayout和extendedLayoutIncludesOpaqueBars呢? 我们不再使用contentInset,因为我们没有使用滚动视图,但是UIKit会做第二好的事情:它将操作我们的topLayoutGuide。 如果导航栏不再是半透明的,该怎么办? 什么都没有移动,但是我们的背景色(实际上是视图本身)不再位于导航栏的后面,并且随着整个视图移到导航栏下方,我们的topLayoutGuide重置为零。 如果我们有一个半透明的导航栏,但在扩展布局中不包括顶部边缘怎么办? 我们的视图不再延伸到导航栏的后面,并且我们的视图移动到导航栏的下方。 如果我们有一个半透明的导航栏,该怎么办? 包括在扩展布局中,但是我们在扩展布局中包含不透明的条形吗? 在这种情况下,我们的视图将在不透明的导航栏后面延伸(因此我们看不到它的后面),但是我们对topLayoutGuide进行了调整,以适应NavigationBar /状态栏。 外观上没有什么区别,但是在幕后,所有属性都得到了适当的尊重。

综上所述,UIViewController的automaticAdjustsScrollViewInsets,extendedLayoutIncludesOpaqueBars和edgeForExtendedLayouts以及它们与半透明和非半透明导航栏的交互方式现在应该更有意义。 我们可以看到UIScrollViews使用contentInset向下移动视图,非UIScrollViews将使用移动布局指南相应地向上/向下移动视图。 UITableViewControllers会将其UITableViews限制在其布局指南的顶部,而不是底部,以便我们可以在导航栏后面滚动,但是非滚动视图是根据我们讨论的各种属性固定的。

只要我们将视图适当地限制在布局指南中,而不仅仅是顶部锚,我们的视图控制器(及其关联的视图)就应该表现出Apple在2013年iOS 7首次发布时的最初意图。


这里的行为很奇怪,但值得注意。 如前所述, automaticAdjustsScrollViewInsets将应用于通过上述magicView()测试的任何视图。 如果您的子视图是受滚动影响的滚动视图,则您可能希望将滚动视图限制为self.view.topAnchor而不是self.topLayoutGuide.bottomAnchor来模仿Apple的行为,这显然是他们希望开发人员使用的行为。 据我所知,除了我已经描述的内容外,这里没有任何特殊的规则可以考虑。


在iOS 11中,Apple推出了safeAreas的概念,在此过程中自动弃用了AdjustsScrollViewInsets。 UIScrollView在此过程中获得了UIScrollViewContentInsetAdjustmentBehavior,这对上述所有工作方式都有一些重要影响。 不过,在我看来,重要的是我们了解这种逻辑的起点。 一旦牢牢扎根于我们的脑海,我们便可以将新的API视作只是起点的一个增量,而不是完全独立地学习iOS 10和iOS 11的行为。 我将发表一篇帖子,用上面很快看到的所有内容解释iOS 11的行为。 发布新文章时,我将使用指向新文章的链接进行更新。


  • 如果self.view是一个UIScrollView或self.view.subviews是一个UIScrollView,或者self.view.subviews.first.subviews是一个UIScrollView,或者[[]],则autoAdjustsScrollViewInsets将适用
  • 如果顶部边缘的扩展布局有效,则自动AdjustsScrollViewInsets将向该属性影响的滚动视图添加一个{navbarHeight}的contentInset。
  • 如果(视图的顶部具有扩展的布局) 并且 (导航栏是半透明的,或者extendedLayoutIncludesOpaqueBars为true),则视图的框架将在导航栏下方扩展
  • 顶部边缘上的扩展布局(及其各种规则/属性)将调整视图控制器的topLayoutGuide所在的位置

如果我在上面犯了一个错误,请随时在Twitter @Wailord上与我联系,或者我可以帮助澄清任何事情。