第十三章——栈视图【译】

在这本书中,您一直在使用自动布局来创建灵活的界面,可以跨设备类型和大小进行扩展。 自动布局是一种非常强大的技术,但是这种能力带来了复杂性。 布置一个界面通常需要大量的约束,并且由于需要不断地添加和移除约束,所以创建动态界面是很困难的。

通常,界面(或界面的子部分)可以以线性方式布局。 考虑一下您编写的其他应用程序:第1章的 Quiz 应用程序由四个子视图组成,它们是垂直布局的。 对于 WorldTrotter 应用程序来说也是如此; ConversionViewController 有一个垂直界面,由一个文本字段和几个标签组成。

具有线性布局的界面非常适合使用 栈视图(stack view)。 栈视图是 UIStackView 的一个实例,它允许您创建一个垂直的或水平的布局,它很容易布局,并且会自动管理您通常需要自己去管理的大部分约束。 也许最重要的是,您可以在其他栈视图中嵌套栈视图,这使您能够在很短的时间内创建真正令人惊叹的界面。

在本章中,您将继续使用 Homepwner 创建一个界面,以显示特定 Item 的详细信息。 您创建的界面将由多个嵌套的栈视图组成,包括垂直和水平的视图(图13.1)。

图13.1 Homepwner和堆栈视图

第十三章——栈视图【译】_第1张图片

使用UIStackView

您将创建一个用于编辑 Item 详细信息的界面。 您将在本章中完成基本界面,然后您将在第14章中完成实施细节。

在顶层,您将拥有一个垂直的堆栈视图,其中包含四个元素,显示 item 的名称(Name)、序列号(Serial)、值(Value)和创建日期(Date Created)(图13.2)。

图13.2 垂直栈视图布局

第十三章——栈视图【译】_第2张图片

打开您的 Homepwner 项目,然后打开 Main.storyboard。 将一个新的 View Controller 从对象库拖到画布上。 将 Vertical Stack View 从对象库拖动到 View ControllerView 上。 向栈视图添加约束上下左右固定为 8 点。

现在将 UILabel 的 4 个实例从对象库拖放到栈视图中。 从上到下,给这些标签添加文本 “Name”、“Serial”、“Value” 和 “Date Created”(图13.3)。

图13.3添加标签到栈视图中

第十三章——栈视图【译】_第3张图片

您可以马上看到问题:标签都有一个红色边框(表示自动布局问题),并且有一个警告,一些视图是垂直不明确的。 有两种方法可以解决这个问题:使用自动布局或修改栈视图的属性。 我们首先使用自动布局解决方案,因为它突出显示了自动布局的重要性。

隐式约束

你在第3章学到,每个视图都有一个固有的内容大小。 您还了解到,如果不指定明确确定宽度或高度的约束,则视图将从其固有内容大小中派生出其宽度或高度。 这是如何实现的呢?

它使用从视图的 content hugging prioritiescontent compression resistance priorities 的隐式约束来完成这一任务。 每个视图(View)的每个轴都有以下优先级之一:

  • horizontal content hugging priority
  • vertical content hugging priority
  • horizontal content compression resistance priority
  • vertical content compression resistance priority

Content hugging priorities

Content hugging priorities 的内容就像放置在视图周围的橡皮筋。 橡皮筋使得视图不想大于其尺寸中的内在内容大小。 每个优先级与0到1000之间的值相关联。值为1000表示视图不能比该维度上的内在内容大小更大。

让我们来看一个只有水平维度的例子。 假设您有两个标签彼此相邻,两个视图之间以及每个视图及其父级视图之间的约束如图13.4所示。

图13.4 两个标签并排

第十三章——栈视图【译】_第4张图片

在父视图变得越来越大之前这样显示看起来还很不错。 但到了那时候哪个标签应该变得更宽? 第一个标签,第二个标签,还是两者? 如图13.5所示,界面目前是模糊的。

图13.5 模糊的布局

第十三章——栈视图【译】_第5张图片

这就是 content hugging priority。 具有较高 content hugging priority 的视图是不拉伸的视图。 你可以把优先值看作是橡皮筋的 “强度”。 优先级越高,橡皮筋越坚固,它想要挤压它的内在内容的尺寸就越大。
注:也就是优该值越大,越难拉伸

Content compression resistance priorities

Content compression resistance priorities 决定了一个视图拒绝小于其固有内容大小的程度。 请考虑图13.4中的相同的两个标签。 如果父级视图的宽度减小了会发生什么? 其中一个标签需要截断它的文本(图13.6)。 但是是哪一个呢?

图13.6 被压缩的模糊布局

第十三章——栈视图【译】_第6张图片

具有较高 Content compression resistance priorities 的视图是能够抵抗压缩的视图,因此不会截断其文本。

有了这些知识,您现在就可以用栈视图来解决问题了。

选择 Date Created 标签并打开其大小(Size)检查器。 找到 Vertical Content Hugging Priority 并将其降低到249.现在其他三个标签拥有更高的 content hugging priority,所以他们将挤压其内在的内容高度。 Date Created 标签将伸展以填充剩余空间。

栈视图 distribution

我们来看看另一种解决问题的方法。 堆栈视图具有可确定其内容布局方式的多个属性。

在画布上或使用文档大纲选择栈视图。 打开属性检查器,找到顶部标记为 Stack View 的部分。 决定内容如何布局的属性之一是 Distribution 属性。 目前它被设置为 Fill,它允许视图根据其固有的内容大小来排列其内容。 将值更改为 Fill Equally。 这将调整标签的大小,使它们都具有相同的高度,忽略内在的内容大小(图13.7)。 要想知道栈视图可以具有的其他的 distribution 值,请阅读文档。

图13.7堆栈视图设置为fill equally

第十三章——栈视图【译】_第7张图片

将堆栈视图的 Distribution 更改为 Fill; 这是你在这一章中想要的。

嵌套堆栈视图

栈视图最强大的功能之一是它们可以彼此嵌套。 您将使用此方式在较大的垂直栈视图中嵌套水平栈视图。 前三个标签将在其旁边显示一个文本字段,显示该 Item 的相应值,并允许用户编辑该值。

在画布上选择 Name 标签。 单击自动布局约束菜单中左侧的第二个图标:


这将将所选视图嵌入到栈视图中。

选择新的栈视图并打开其属性检查器。 栈视图当前是一个垂直栈视图,但您希望它是一个水平栈视图。 将 Axis 更改为 Horizontal

现在将 Text Field 从对象库拖到 Name 标签的右侧。 因为默认情况下,标签拥有比文本字段更大的 content hugging priority ,所以标签会挤压内在的内容宽度并且文本字段会延伸。 标签和文本字段目前具有相同的内容压缩阻力优先级,如果文本字段的文本太长,这将导致模糊的布局。 打开文本字段的大小检查器,并将其 Horizontal Content Compression Resistance Priority 设置为 749。这将确保文本字段的文本将在必要时被截断,而不是标签。

栈视图间隔

标签和文本框看起来有点小,因为它们之间没有间隔。 栈视图允许您自定义 item 之间的间隔。

选择水平栈视图并打开它的属性检查器。 改变 Spacing8。 注意,文本字段收缩以适应间隔,因为它对压缩的抵抗力比标签要小。

SerialValue 标签重复以下步骤:

  1. 选择标签,然后单击

    图标。

  2. 将堆栈视图更改为水平(horizontal)栈视图。
  3. Text field 拖到水平栈视图上,将其 horizontal content compression resistance priority 改为749
  4. 更新堆栈视图,Spacing8 点。

你会想要对界面进行另外的调整:垂直栈视图需要一些间距。 Date Created 标签应具有中心文本对齐方式。 NameSerialValue 标签的宽度应该相同。

选择垂直栈视图,打开其属性检查器,并将 Spacing 更新为 8 点。 然后选择 Date Created 的标签,打开其属性检查器,并将 Alignment 更改为居中。 这解决了前两个问题。

虽然栈视图大大减少了您需要添加到界面中的约束数量,但有一些约束仍然很重要。 在界面上,由于标签宽度的差异,文本字段的前部不对齐。 (英文中的区别不是非常明显,但当本地化为其他语言时,这种差异变得更加显着)为了解决这个问题,您将在三个文本字段之间添加前部约束。

右键选中 Name 文本字段拖动到 Seria 文本字段,并选择 Leading。 然后对 Serial 文本字段和 Value 文本字段执行相同操作。 完成的界面如图13.8所示。

图13.8最终栈视图界面

第十三章——栈视图【译】_第8张图片

栈视图会使你在短时间内就能创建非常丰富的界面,比您在使用手动配置约束需要的时间要短。 约束仍然要被添加,但是它们是由堆栈视图本身去管理,而不是你。 栈视图允许您在运行时具有非常动态的界面。 您可以通过使用 addArrangedSubview(_ :)insertArrangedSubview(_:at :)removeArrangedSubview(_ :) 来添加和删除视图。 您还可以在栈视图中的视图上切换 hidden 属性。 堆栈视图将通过该值自动布局其内容。

Segues

大多数iOS应用程序都有许多视图控制器,用户可以在其中进行导航。故事板允许您将这些交互设置为 segues,而无需编写代码。

一个 segue 能将另一个视图控制器的视图移到屏幕上,并由 UIStoryboardSegue 的一个实例来表示。 每个 segue 都有一个 样式(style)、一个 动作项(action item) 和一个 标识符(identifier)。 segue 的样式决定了视图控制器的呈现方式。 动作项是在故事板文件中触发 segue 的视图对象,如按钮、表视图单元或其他 UIControl。 标识符用于以编程方式访问 segue。 当您想要触发一个不是来自某个动作项的 segue 时,这是很有用的,比如抖动或其他无法在故事板文件中设置的界面元素。

让我们从一个 show segue开始。 show segue 显示一个视图控制器,这取决于它显示的上下文。 segue 将在表视图控制器和新视图控制器之间。 动作项将是表视图的 cell; 点击一个 cell 将会显示视图控制器。

Main.storyboard 中,在 Items View Controller 上选择 ItemCell 的 prototype cell。 右键从 cell 拖动到上一节中新设置的视图控制器。 (确保您是从 cell 拖动而不是表视图!)接着将出现一个黑色面板,其中列出了可能的样式。 从 Selection Segue 部分中选择 Show(图13.9)。

图13.9 设置一个 show segue

第十三章——栈视图【译】_第9张图片

注意从表视图控制器到新视图控制器的箭头。 这是一个 segue。 圆圈中的图标告诉你这个 segue 是一个 show segue,每个 segue 都有一个唯一的图标。

构建并运行应用程序。 点击 cell,新的视图控制器将从屏幕底部向上滑动。 (从底部滑出是以模态方式呈现视图控制器时的默认行为。)

到现在为止还挺好! 但目前有两个问题:视图控制器不显示所选 Item 的信息,并且无法关闭视图控制器以返回到 ItemsViewController。 您将在下一节中解决第一个问题,在第14章中解决第二个问题。

挂钩内容

要显示所选 Item 的信息,您将需要创建一个新的 UIViewController 子类。

创建一个新的 Swift文件 并将其命名为 DetailViewController。 打开 DetailViewController.swift 并声明一个名为 DetailViewController 的新 UIViewController 子类。

import Foundation
import UIKit

class DetailViewController: UIViewController {

}

因为您需要能够访问您在运行时创建的子视图,DetailViewController 需要它们的 outlet。 该计划是向 DetailViewController 添加四个新 outlet,然后进行连接。 在以前的练习中,您已经做了两个不同的步骤:首先,您在 Swift 文件中添加 outlet,然后在故事板文件中进行了连接。 其实您可以使用助理编辑器一次完成。

打开 DetailViewController.swift,在项目导航器中选择 Main.stroyboard。 这将打开在 DetailViewController.swift 旁边的助理编辑器中的文件。 (您可以通过单击工作区顶部的 Editor 控件中的中间按钮来切换助理编辑器,显示助理编辑器的快捷方式为 Command-Option-Return,返回标准编辑器的快捷方式为 Command-Return。)

你的窗口已经变得有些凌乱。 让我们做一些暂时的修改。 通过单击工作区顶部的 View控件中的左侧按钮隐藏导航区 (此快捷方式为 Command-0)。 然后,通过单击编辑器左下角的切换按钮(右上角左边那三个按钮的中间的那个显示像两个圆交叉的那个按钮),在 Interface Builder 中隐藏文档轮廓。 您的工作区现在应如图13.10所示。

图13.10 布局工作区

第十三章——栈视图【译】_第10张图片

在连接 outlet 之前,您需要告知详细界面应该与 DetailViewController 相关联。 在画布上选择 View Controller 并打开其身份检查器。 将 Class 更改为 DetailViewController(图13.11)。

图13.11 设置视图控制器类

第十三章——栈视图【译】_第11张图片

UITextField 的三个实例和 UILabel 的底部实例将在 DetailViewController 中显示。 右键选中从 Name 标签旁边的 UITextField 拖动到 DetailViewController.swift的顶部,如图13.12所示。

图13.12 从故事板拖动到源文件

第十三章——栈视图【译】_第12张图片

放手后会出现弹出窗口。 在 Name 字段中输入 nameField,确保 Storage 设置为 Strong,然后单击 Connect(图13.13)。

图13.13 自动生成outlet并进行连接

第十三章——栈视图【译】_第13张图片

这将在 DetailViewController 中创建一个名为 nameField 的类型为 UITextField@IBOutlet 属性。

此外,这个 UITextField 已经连接到 DetailViewControllernameField outlet。 您可以通过单击 Detail View Controller 查看连接来验证这一点。 还要注意,将鼠标悬停在面板上的 nameField 连接之上,将会显示您连接的 UITextField

以相同的方式创建其他三个 outlet,并将其命名,如图13.14所示。

图13.14连接图

第十三章——栈视图【译】_第14张图片

进行连接后,DetailViewController.swift 应该如下所示:

import UIKit

class DetailViewController: UIViewController {

  @IBOutlet var nameField: UITextField!
  @IBOutlet var serialNumberField: UITextField!
  @IBOutlet var valueField: UITextField!
  @IBOutlet var dateLabel: UILabel!

}

如果您的文件看起来不一样,那表明您的 outlet 连接不正确。 解决您的文件与上述代码之间的任何差异的三个步骤:首先,通过 右键——拖动 流程并再次进行连接,直到您在 DetailViewController.swift 中显示上述四行。 第二,删除创建的任何错误代码(如非属性方法声明或属性)。 最后,检查故事板文件中的任何不良连接——在 Main.storyboard 中,右击 Detail View Controller*。 如果任何连接旁边都有黄色警告标志,请点击这些连接旁边的x` 图标以将其断开连接。

确保界面文件中没有错误的连接很重要。 当更改属性名称但不更新界面文件中的连接或完全删除属性但不从界面文件中删除时,通常会发生连接错误。 无论哪种方式,连接错误都会导致应用程序在加载界面文件时崩溃。

连接完成后,您可以关闭助理编辑器,并返回到仅查看 DetailViewController.swift

DetailViewController 将持有对正在显示的 Item 的引用。 加载视图后,您将会将每个文本字段上的文本从 Item 实例中设置为适当的值。

DetailViewController.swift 中,为 Item 实例添加一个属性,并重写 viewWillAppear(_ :) 来设置界面。

class DetailViewController: UIViewController {

  @IBOutlet var nameField: UITextField!
  @IBOutlet var serialNumberField: UITextField!
  @IBOutlet var valueField: UITextField!
  @IBOutlet var dateLabel: UILabel!

  var item: Item!

  override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)

    nameField.text = item.name
    serialNumberField.text = item.serialNumber
    valueField.text = "\(item.valueInDollars)"
    dateLabel.text = "\(item.dateCreated)"
  }
}

这是有用的,我们最好使用格式化器而不是使用字符串插值来打印出 valueInDollarsdateCreated。 您在第 4 章中使用了 NumberFormatter 的实例。您将在此使用另一个实例,以及 DateFormatter 的实例来格式化 dateCreated

NumberFormatterDateFormatter 的实例添加到 DetailViewController。 在 viewWillAppear(_ :) 中使用这些格式化器来格式化 valueInDollarsdateCreated

var item: Item!

let numberFormatter: NumberFormatter = {
  let formatter = NumberFormatter()formatter.numberStyle = .decimal
  formatter.minimumFractionDigits = 2
  formatter.maximumFractionDigits = 2
  return formatter
}()

let dateFormatter: DateFormatter = {
  let formatter = DateFormatter()
  formatter.dateStyle = .medium
  formatter.timeStyle = .none
  return formatter
}()

override func viewWillAppear(_ animated: Bool) {
  super.viewWillAppear(animated)

  nameField.text = item.name
  serialNumberField.text = item.serialNumber
  valueField.text = "\(item.valueInDollars)"
  dateLabel.text = "\(item.dateCreated)"
  valueField.text = numberFormatter.string(from: NSNumber(value: item.valueInDollars))
  dateLabel.text = dateFormatter.string(from: item.dateCreated)
}

传递数据

当表视图中的一行被点击时,您需要一个告诉 DetailViewController 当前选择了哪个 Item 的方法。 每当触发 segue 时,都会在启动 segue 的视图控制器上调用 prepare(for:sender :) 方法。 该方法有两个参数:UIStoryboardSegue——它提供有关哪个 segue 正在发生的信息,以及 发送者(sender)——它是触发 segue 的对象(例如 UITableViewCellUIButton)。

UIStoryboardSegue 为您提供了三条信息:源视图控制器(segue 起始位置),目标视图控制器(segue 结束位置)以及 segue 的标识符。 标识符可以区分分隔符。 让我们给 segue 一个有用的标识符。

再次打开 Main.storyboard。 通过单击两个视图控制器之间的箭头并打开属性检查器来选择 show segue。 对于 identifier,输入 showItem(图13.15)。

图13.15 Segue 标识符

第十三章——栈视图【译】_第15张图片

随着学习的深入,你现在可以传递你的 Item 实例。 打开 ItemsViewController.swift并实现 prepare(for:sender :)

override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
  // If the triggered segue is the "showItem" segue
  switch segue.identifier {
  case "showItem"?:
    // Figure out which row was just tapped
    if let row = tableView.indexPathForSelectedRow?.row {

      // Get the item associated with this row and pass it along
      let item = itemStore.allItems[row]
      let detailViewController = segue.destination as! DetailViewController
      detailViewController.item = item
    }
  default:
    preconditionFailure("Unexpected segue identifier.")
  }
}

您在第2章中了解了 switch 语句。这里,您正在使用它来切换可能的 segue 标识符。 因为 segue 的 identifier 是一个可选的String, 因为其后面加上了?。 默认块使用 preconditionFailure(_ :) 函数捕获任何意外的 segue 标识符并使应用程序崩溃。 如果程序员忘记给出一个标识符,或者如果在某个地方出现了一个带有 segue 标识符的错字,那就会出现这种情况。 任何一种情况都是程序员的错误,使用 preconditionFailure(_ :) 可以帮助您更快地识别这些问题。

构建并运行应用程序。 点击一行,DetailViewController 将在屏幕上滑动,显示该 item 的详细信息。 (您将在第14章中修复无法返回 ItemsViewController 的问题)

许多新加入iOS的程序员都在与视图控制器之间如何传递数据进行斗争。 将根视图控制器中的所有数据并将数据的子集传递给下一个 UIViewController (就像本章所做的那样) 是一种简洁高效的方法。

青铜挑战:更多栈视图

栈视图对 QuizWorldTrotter 也很好用。 使用 UIStackView 更新这两个应用程序。

你可能感兴趣的:(第十三章——栈视图【译】)