UIKit框架(二十一) —— UIStackView的使用(一)

版本记录

版本号 时间
V1.0 2019.07.05 星期五

前言

iOS中有关视图控件用户能看到的都在UIKit框架里面,用户交互也是通过UIKit进行的。感兴趣的参考上面几篇文章。
1. UIKit框架(一) —— UIKit动力学和移动效果(一)
2. UIKit框架(二) —— UIKit动力学和移动效果(二)
3. UIKit框架(三) —— UICollectionViewCell的扩张效果的实现(一)
4. UIKit框架(四) —— UICollectionViewCell的扩张效果的实现(二)
5. UIKit框架(五) —— 自定义控件:可重复使用的滑块(一)
6. UIKit框架(六) —— 自定义控件:可重复使用的滑块(二)
7. UIKit框架(七) —— 动态尺寸UITableViewCell的实现(一)
8. UIKit框架(八) —— 动态尺寸UITableViewCell的实现(二)
9. UIKit框架(九) —— UICollectionView的数据异步预加载(一)
10. UIKit框架(十) —— UICollectionView的数据异步预加载(二)
11. UIKit框架(十一) —— UICollectionView的重用、选择和重排序(一)
12. UIKit框架(十二) —— UICollectionView的重用、选择和重排序(二)
13. UIKit框架(十三) —— 如何创建自己的侧滑式面板导航(一)
14. UIKit框架(十四) —— 如何创建自己的侧滑式面板导航(二)
15. UIKit框架(十五) —— 基于自定义UICollectionViewLayout布局的简单示例(一)
16. UIKit框架(十六) —— 基于自定义UICollectionViewLayout布局的简单示例(二)
17. UIKit框架(十七) —— 基于自定义UICollectionViewLayout布局的简单示例(三)
18. UIKit框架(十八) —— 基于CALayer属性的一种3D边栏动画的实现(一)
19. UIKit框架(十九) —— 基于CALayer属性的一种3D边栏动画的实现(二)
20. UIKit框架(二十) —— 基于UILabel跑马灯类似效果的实现(一)

开始

首先看下写作环境

Swift 5, iOS 12, Xcode 10

主要内容:了解如何使用UIStackView简化iOS布局。 使用对齐,分布和间距水平或垂直布局一系列视图。

您是否曾经需要在运行时添加或删除视图中的视图并调整旁边的视图布局? 也许您调整了一些约束或使用第三方库来完成这项工作。 也许它不是运行时的东西,而是你希望在故事板中的其他视图之间插入一个新视图。

在这些情况下,您需要更改多个约束。 您可能会发现自己删除了该区域中的所有约束并重新添加它们。

UIStackView简化了这些任务。 您可以在堆栈视图中轻松地水平或垂直布局一系列视图,并使用诸如对齐,分布和间距等属性将视图自动调整到可用空间的方式进行设置。 请享用!

打开准备好的项目材料。 使用iPhone 8 Simulator在Xcode中构建并运行启动项目。 你会看到以下内容:

这是Vacation Spots应用程序。 它建议了一系列可以远离它的地方。 在去任何地方之前,您将使用Stack Views解决应用程序的一些问题。

1. Exploring Vacation Spots

点击London,进入伦敦的信息视图。 乍一看,视图可能看起来不错,但它有一些问题:

  • 1) 查看视图底部的按钮行。 由于它们之间的固定间距,它们不适应屏幕宽度。 要更好地查看问题,请按Command-left箭头将模拟器旋转到横向。
  • 2) 点击WEATHER旁边的Hide。 这会隐藏文本,但它不会重新布局它下面的部分,留下一块空白区域。
  • 3) 区域内容需要改进。 如果在WHY VISIT之后出现了WHAT TO SEE,而不是在它们之间定位天气部分,那将更符合逻辑。
  • 4) 在横向模式下,底行按钮太靠近视图的下边缘。 如果减小section之间的间距,它看起来会更好 - 但仅限于横向模式。

现在您已经了解了将要进行的改进,现在是时候深入了解该项目。 打开Main.storyboard。 它将以初始设备视图打开。 确保它将iPhone 8显示为所选设备。

这在运行时没有任何影响,但可以帮助您了解屏幕在该设备上的外观。 您可以通过单击其他图标随时更改所选设备。 将鼠标悬停在图标上可显示其相应的设备。

注意:如果减小Interface Builder画布宽度(大约低于650像素),则更改设备的UI会稍微改变。 设备列表折叠为Device下拉列表:

当按下时,它会显示垂直布局的可用设备列表:

现在,仔细查看Spot Info View Controller

你可能想知道颜色是什么?

这些标签和按钮具有不会在运行时显示的背景颜色。 在故事板中,它们是视觉辅助工具,可帮助显示更改堆栈视图的各种属性如何影响其嵌入视图的框架。

如果在运行应用程序时您想在任何时候查看背景颜色,您可以在SpotInfoViewController内的viewDidLoad()中临时注释掉以下行。

// Clear background colors from labels and buttons
for view in backgroundColoredViews {
  view.backgroundColor = UIColor.clear
}

任何outlet连接的label都有一个与outlet变量名称匹配的占位符文本。 这样可以更容易地确定哪些标签将在运行时更新其文本。 例如,带有文本<whyVisitLabel>的标签连接到:

@IBOutlet var whyVisitLabel: UILabel!

是时候开始了!


Your First Stack View

首先,您将修复底行按钮之间的间距。 堆栈视图可以以各种方式沿其轴分布其子视图。 一种方法是在每个视图之间设置相等的间距。

幸运的是,将现有视图嵌入到新的stack view中是件小事。 首先,单击Spot Info View Controller场景底部的所有按钮,然后单击其中一个按钮,然后command-clicking其他两个按钮。 使用故事板画布左下角的Show Document Outline按钮打开大纲视图。

确认您已选择所有三个按钮。

它们在大纲视图中突出显示。 您还可以command-click大纲视图中的每个按钮以选择它们。

选择后,单击故事板画布右下角的Auto Layout工具栏中的Embed In按钮。 将出现一个包含可用嵌入选项的菜单。 选择堆栈视图:

Xcode会将按钮嵌入到新的堆栈视图中。

1. Stack View Constraints

虽然堆栈视图负责定位按钮,但仍需要添加自动布局约束来定位堆栈视图本身。

在堆栈视图中嵌入视图时,它会丢失对其他视图的约束。 例如,在将按钮嵌入堆栈视图之前,Submit Rating按钮的顶部在RATING标签的底部有一个垂直间距约束:

选择Submit Rating按钮并验证它是否不再附加任何约束:

您还可以查看Size inspector (Option-Command-5)以验证没有约束:

有时,在拥挤的故事板的视图控制器中选择特定元素很困难。 您可以从outline view中选择元素,或使用Shift并右键单击或按住Control键并单击要选择的视图。 这将为您提供一个上下文菜单,显示您单击的位置的视图层次结构。 在菜单中单击选择stack view

现在您已选择Stack View,您可以为其添加约束。

单击Auto Layout工具栏中的Add New Constraints按钮以显示Add New Constraints弹出窗口。

首先,为Constrain to margins添加一个选中。 然后,将以下约束添加到堆栈视图的边缘:

Top: 20, Leading: 0, Trailing: 0, Bottom: 0

仔细检查top, leading, trailing, and bottom约束的数字。 确保已打开四个红色I-beamsConstrain to margins。 然后,单击Add 4 Constraints

现在堆栈视图的大小正确,但它已拉伸第一个按钮以填充任何额外空间。

2. Stack View Appearance

确定堆栈视图如何沿其轴布置其子视图的属性是分布。 目前,它设置为Fill,这意味着子视图将沿其轴完全填充堆栈视图。 要实现此目的,堆栈视图将仅展开其子视图之一以填充该额外空间。 具体来说,它对最低的horizontal content hugging priority的视图进行扩展,或者如果所有优先级相等,则扩展第一个视图。

但是,您不希望按钮完全填充堆栈视图 - 您希望它们间隔相等。

确保仍然选中堆栈视图并转到Attributes inspector检查器。 将DistributionFill更改为Equal Spacing

现在构建并运行,点击任何单元格,然后旋转模拟器。 您会看到底部按钮现在可以平等分配!

实现UI时,请始终询问自己元素是否适合可用空间。

以下是您的内容如何影响用户体验的示例:将Wikipedia按钮的标题更改为Wikipedia website并运行该应用程序。 将模拟器从纵向旋转到横向以查看差异。

在空间更紧凑的纵向方向上,最后一个按钮中的文本被剪裁,而在横屏中一切都很合适。

这是您经常遇到的问题,尤其是iPhone SE等较窄的屏幕尺寸。

幸运的是,这是一个简单的解决方案。 在仍然选择堆栈视图的情况下,返回Attributes inspector。 将比例从Equal Spacing更改为Fill Proportionally并将间距值更改为10

现在,构建并运行并检查两个方向。 你会发现一切都好了。

将按钮的名称更改回Wikipedia

恭喜,您已经构建了第一个堆栈视图!

3. Without a Stack View

为了在没有堆栈视图的情况下解决此间距问题,您必须在每对按钮之间使用一个间隔视图,为所有间隔视图添加相等的宽度约束,以及正确定位间隔视图的若干附加约束。

这看起来像下面这样。 为了在屏幕截图中显示,间隔视图显示浅灰色背景:

如果您必须在故事板中执行此操作,那么这不是一个问题,但许多视图都是动态的。 由于相邻的间隔视图和约束,在运行时添加新按钮或隐藏或删除现有按钮并不是一项简单的任务。

要在堆栈视图中隐藏视图,请将包含视图的hidden属性设置为true,并使用堆栈视图处理其余视图。 这是当用户隐藏其下方的文本时,您将如何修复WEATHER标签下的间距。 将weather部分标签添加到堆栈视图后,您将在本教程中执行此操作。

注意:您现在知道如何指定堆栈中子视图之间的间距。 但是如果你想在特定的子视图后有不同的间距呢? 从iOS 11开始,您可以使用 setCustomSpacing:afterView来实现。


Converting the Sections

接下来,您将转换SpotInfoViewController中的所有其他部分以使用堆栈视图。 这使您可以完成剩余的任务。 从rating部分开始。

1. Converting the Rating Section

在您创建的堆栈视图上方,选择RATING标签和旁边的星号标签。

然后,单击Editor ▸ Embed in ▸ Stack View

现在,选择新创建的堆栈视图,然后再次单击Add New Constraints按钮。 选中Constrain to margins并添加以下三个约束:

Top: 20, Leading: 0, Bottom: 20

现在转到Attributes inspector并将间距设置为8

注意:您可能已经注意到间距已经设置为8。以前,间距自动设置为24,Xcode已经变得足够聪明,可以推断间距值以匹配嵌入视图之前的间距值。 很酷,对吗?

构建并运行以验证所有内容与以前相同。

2. Un-embedding a Stack View

在你走得太远之前,最好在出现问题时进行一些急救培训。 例如,您可能会发现自己有一个额外的堆栈视图,可能是因为实验,重构或意外。

幸运的是,从堆栈视图中可以轻松地取消嵌入视图。

首先,选择要删除的堆栈视图。 单击Embed In按钮。 接下来,在出现的上下文菜单中选择Unembed

另一种方法是选择堆栈视图,然后从菜单中选择Editor ▸ Unembed

3. Creating a Vertical Stack View

现在,创建第一个垂直堆栈视图。 选择WHY VISIT标签和其下方的<whyVisitLabel>,然后选择Embed in ▸ Stack View

当您在堆栈视图中嵌入标签时,Xcode会根据标签的位置推断它应该是一个垂直堆栈。

较低的标签之前有一个约束将其固定到右边距,但是在堆栈视图中嵌入标签会删除该约束。 目前,堆栈视图没有约束,因此它采用其最大视图的固有宽度。

选择堆栈视图后,单击Add New Constraints按钮。 将Top,LeadingTrailing约束设置为0,再次确保选中Constrain to margins

然后,单击底部约束右侧的下拉菜单并选择WEATHER (current distance = 20)

默认情况下,Interface Builder会向您显示最近邻居的约束,对于底部约束,它是距离为15Hide按钮。您需要约束到其下方的WEATHER标签。

最后,单击Add 4 Constraints。 您现在应该看到以下内容:

现在您有一个扩展的堆栈视图,右边缘固定到视图的右边缘。 但是,底部标签的宽度仍然相同。 您将通过更新堆栈视图的alignment属性来解决此问题。

4. Alignment Property

alignment属性确定堆栈视图如何垂直于其轴放置其视图。 对于垂直堆栈视图,可能的值为Fill,Leading,CenterTrailing

水平堆栈视图的可能alignment值略有不同:

水平堆栈视图有.top而不是.leading,并且有.bottom而不是.trailing。 还有两个属性仅在水平方向有效,.firstBaseline.lastBaseline

选择每个值以查看它如何影响垂直堆栈视图中的标签放置:

Fill:

Leading:

Center:

Trailing:

完成对每个值的测试后,将Alignment设置为Fill

构建并运行以验证一切看起来都很好并且没有回归。

指定Fill意味着您希望所有视图都垂直于其轴填充堆栈视图。 这会导致WHY VISIT标签也扩展到右边缘。

如果您只希望底部标签扩展到边缘怎么办?

现在,它并不重要,因为两个标签在运行时都有清晰的背景,但是当你转换weather部分时它会很重要。

您将学习如何使用额外的堆栈视图来实现这一目标。

5. Converting the What to See Section

转换此部分与您已经完成的操作类似。

  • 1) 首先,选择WHAT TO SEE标签和下面的<whatToSeeLabel>
  • 2) 单击Embed In按钮,然后选择Stack View
  • 3) 单击Pin按钮。
  • 4) 选中Constrain to margins并添加以下四个约束:
Top: 20, Leading: 0, Trailing: 0, Bottom: 20
  • 5) 将堆栈视图的Alignment设置为Fill

您的故事板现在应该如下所示:

这只剩下weather部分了。


Converting the Weather Section

由于Hide按钮,weather部分比其他部分更复杂。

要转换weather部分,您可以通过将WEATHER标签和Hide按钮嵌入水平堆栈视图来创建嵌套堆栈视图。 然后,将该水平堆栈视图和<weatherInfoLabel>嵌入到垂直堆栈视图中。

它看起来像这样:

请注意,WEATHER标签已扩展为等于Hide按钮的高度。 这将导致WEATHER标签的基线与其下方的文本之间产生额外的空间。

请记住,alignment指定垂直于堆栈视图的位置。 因此,您可以将alignment设置为Bottom

但是,您不希望Hide按钮的高度决定堆栈视图的高度。

相反,您将从所有堆栈视图中删除Hide按钮。

Hide按钮仍将是顶级视图的子视图,您将从其中添加一个约束到WEATHER标签 - 它将位于堆栈视图中。 没错,您将从堆栈视图外部的按钮添加约束到堆栈视图中的标签!

选择WEATHER标签和它下面的<weatherInfoLabel>,然后将它们堆叠在堆栈视图中。

单击Add New Constraints按钮,选中Constrain to margins并添加以下四个约束:

Top: 20, Leading: 0, Trailing: 0, Bottom: 20

将堆栈视图的Alignment设置为Fill

您需要在Hide按钮的左边缘和WEATHER标签的右边缘之间存在约束,因此使用WEATHER标签填充堆栈视图将不起作用。

但是,您确实希望底部的<weatherInfoLabel>填充堆栈视图。

您可以通过将WEATHER标签仅嵌入垂直堆栈视图来实现此目的。 请记住,垂直堆栈视图的alignment可以设置为.leading,如果您将堆栈视图拉伸超出其固有宽度,则其子视图将保持与前端对齐。

使用document outline选择WEATHER标签,或使用Control-Shift-click方法。 然后,将其嵌入新的堆栈视图中。

Alignment设置为Leading,并确保Axis设置为Vertical

很好! 外部堆栈视图拉伸内部堆栈视图以填充宽度,但内部堆栈视图允许标签保持其原始宽度!

建立并运行,为什么Hide按钮在文本中间悬空?

WEATHER标签嵌入堆栈视图时,它与Hide按钮之间的任何约束都被删除。

要添加新约束,请按住Control键并将Hide按钮拖动到WEATHER标签。

按住Shift键选择多个选项,然后选择Horizontal SpacingFirst Baseline。 然后按Enter,或单击弹出视图外的任何位置:

构建并运行。 现在应该正确定位Hide按钮。 由于设置为隐藏的标签嵌入在堆栈视图中,因此按Hide会隐藏标签并调整其下方的视图 - 所有这些都无需手动调整任何约束。

现在所有部分都在独特的堆栈视图中,您可以将它们嵌入到外部堆栈视图中,这将使最后两个任务变得微不足道。

1. Top-level Stack View

按住Command键并单击以在outline view中选择所有五个顶级堆栈视图。

然后将它们全部嵌入到一个堆栈视图中:

单击Add New Constraints按钮,选中Constrain to margins并向所有边添加0的约束。 然后将Spacing设置为20并将Alignment设置为Fill。 您的故事板场景现在应该如下所示:

构建并运行

WEATHER堆栈视图嵌入外部堆栈视图时,看起来隐藏按钮再次失去其约束。 没问题,只需按照以前的方式再次添加约束:

  • 按住Control键并从Hide按钮拖动到WEATHER标签。
  • 按住Shift键。
  • 选择Horizontal SpacingBaseline
  • Enter,或单击弹出视图外的任何位置。

建立并运行。 Hide按钮现在位于正确的位置。

2. Repositioning Views

现在所有部分都在顶级堆栈视图中,您将修改WHAT TO SEE部分的位置,使其位于WEATHER部分之上。

从大纲视图中选择middle stack view,然后在第一个和第二个视图之间拖动它。

注意:将指针稍微保持在您之间拖动的堆栈视图的左侧,以便它仍然是外部堆栈视图的子视图。 小蓝圈应位于两个堆栈视图之间的左边缘,而不是右边缘:

3. Size Class Configurations

最后,将注意力转移到列表中的剩余任务。 在横向模式下,垂直空间非常重要,因此您希望将堆栈视图的各个部分更紧凑的放在一起。 为此,当垂直size classes是紧凑的时,您将使用size classes将顶级堆栈视图的间距设置为10而不是20。

选择顶级堆栈视图,然后在Attributes inspector中,单击Spacing旁边的+按钮:

选择Any WidthCompact Height,然后选择Add Variation

在新的hC字段中将Spacing设置为10

建立并运行。 纵向模式下的间距应保持不变。 旋转模拟器并注意部分之间的间距已减小,按钮现在从视图底部有足够的空间:

如果你没有添加顶级堆栈视图,你仍然可以使用size classes在分隔五个部分的四个约束中的每一个上将垂直间距设置为10,但是在一个地方设置它不是更好吗?

你有更好的时间做一些有意义的事,比如动画!


Animation

目前,该应用程序在隐藏和显示天气细节时非常跳跃。您将添加一些动画以平滑过渡。

堆栈视图与UIView动画引擎兼容。这意味着动画排列的子视图的外观或消失的动画就像在动画块中切换其isHidden属性一样简单。

更改已排列的子视图的isHidden属性会更新其父堆栈视图的布局。如果该更新位于动画块中,则如果您在动画块中自己更改了布局,则它将是相同的。

是时候写一些代码了!打开SpotInfoViewController.swift并查看updateWeatherInfoViews(hideWeatherInfo:animated :)

你会在方法的最后看到这些行:

weatherHideOrShowButton.setTitle(newButtonTitle, for: .normal)
weatherInfoLabel.hidden = shouldHideWeatherInfo

替换成下面的内容

if animated {
  UIView.animate(withDuration: 0.3) {
    self.weatherHideOrShowButton.setTitle(newButtonTitle, for: .normal)
    self.weatherInfoLabel.isHidden = shouldHideWeatherInfo
  }
} else {
  weatherHideOrShowButton.setTitle(newButtonTitle, for: .normal)
  weatherInfoLabel.isHidden = shouldHideWeatherInfo
}

构建并运行,然后点击HideShow按钮。 动画版本不是更好吗?

除了在堆栈视图中包含的视图上设置隐藏属性的动画之外,还可以在堆栈视图本身上设置动画属性,例如alignment, distribution, spacing 甚至是 axi

后记

本篇主要讲述了UIStackView的使用,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容