# 定义

## 定义子类

您可以使用UIViewController的自定义子类来展示应用程序的内容。大多数自定义视图控制器是内容视图控制器，也就是说，它们拥有所有视图并负责这些视图中的数据。相比之下，容器视图控制器并不拥有其所有视图。它的某些视图由其他视图控制器管理。定义内容和容器视图控制器的大多数步骤是相同的​​，并将在以下各节中进行讨论。

对于内容视图控制器，最常见的父类如下：

* 当视图控制器的主视图是表格时，请专门使用UITableViewController。
* 当视图控制器的主视图是集合视图时，请专门使用UICollectionViewController。
* 将UIViewController用于所有其他视图控制器。

对于容器视图控制器，父类取决于您是要修改现有的容器类还是创建自己的容器类。对于现有容器，请选择要修改的任何视图控制器类。对于新的容器视图控制器，通常可以将UIViewController子类化。

### 定义你的UI

使用Xcode中的情节提要文件直观地为视图控制器定义UI。尽管您也可以通过编程方式创建UI，但情节提要是可视化视图控制器内容并根据不同环境自定义视图层次结构（根据需要）的绝佳方法。以视觉方式构建UI可使您快速进行更改，并无需构建和运行应用即可查看结果。

下图显示了一个故事板的示例。每个矩形区域代表一个视图控制器及其关联的视图。视图控制器之间的箭头是视图控制器的关系和顺序。关系将容器视图控制器连接到其子视图控制器。 Segues使您可以在界面的视图控制器之间导航。

![](https://3323963180-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MSfsKp5x5jHFPTcZRiN%2Fsync%2Fa8e2d2634f69fed81f9e5eb37b6f2be21774a43e.png?generation=1612774877319581\&alt=media)

每个新项目都有一个主情节提要，该情节提要通常已经包含一个或多个视图控制器。您可以通过将新的视图控制器从库中拖动到画布上来将它们添加到情节提要中。新视图控制器最初没有关联的类，因此您必须使用身份检查器分配一个。

使用情节提要编辑器执行以下操作：

* 添加，排列和配置视图控制器的视图。
* 连接outlet和行动；
* 在视图控制器之间创建关系和顺序
* 自定义不同尺寸类别的布局和视图
* 添加手势识别器以处理用户与视图的交互

### 处理用户互动

应用的响应者对象可处理传入事件并采取适当的措施。尽管视图控制器是响应者对象，但是它们很少直接处理触摸事件。相反，视图控制器通常以以下方式处理事件。

* 视图控制器定义用于处理更高级别事件的操作方法。行动方法回应:
  * 具体动作。控件和某些视图调用一种操作方法来报告特定的交互。
  * 手势识别器。手势识别器调用动作方法来报告手势的当前状态。使用视图控制器来处理状态更改或响应完成的手势。
* 视图控制器观察系统或其他对象发送的通知。通知报告更改，并且是视图控制器更新其状态的一种方式。
* 视图控制器充当数据源或代表另一个对象。视图控制器通常用于管理表和集合视图的数据。您也可以将它们用作对象（例如CLLocationManager对象）的委托，该对象将更新的位置值发送到其委托。

响应事件通常涉及更新视图的内容，这需要引用这些视图。视图控制器是为以后需要修改的任何视图定义出口的好地方。下表中所示的语法将网点声明为属性。清单中的定制类定义了两个出口（由IBOutlet关键字指定）和一个动作方法（由IBAction返回类型指定）。出口在情节提要中存储对按钮和文本字段的引用，而操作方法则响应按钮中的轻击。

```
class MyViewController: UIViewController {
    @IBOutlet weak var myButton : UIButton!
    @IBOutlet weak var myTextField : UITextField!

    @IBAction func myButtonAction(sender: id)
}
```

在storyboard中，请记住将视图控制器的输出口和操作连接到相应的视图。连接情节提要文件中的出口和操作可确保在加载视图时对其进行配置。

### 在运行时显示视图

情节提要使加载和显示视图控制器的视图的过程非常简单。 UIKit会在需要时自动从情节提要文件中加载视图。作为加载过程的一部分，UIKit执行以下任务序列： 1. 使用情节提要文件中的信息实例化视图. 2. 连接所有outlet和动作。 3. 将根视图分配给视图控制器的view属性。 4. 调用视图控制器的awakeFromNib方法。 调用此方法时，视图控制器的特征集合为空，并且视图可能不在其最终位置。 5. 调用视图控制器的viewDidLoad方法。 使用此方法可以添加或删除视图，修改布局约束以及为视图加载数据。

在屏幕上显示视图控制器的视图之前，UIKit给您一些额外的机会来在屏幕上之前和之后准备这些视图。具体来说，UIKit执行以下任务序列： 1. 调用视图控制器的viewWillAppear：方法以使其知道其视图即将显示在屏幕上。 2. 更新视图的布局。 3. 在屏幕上显示视图。 4. 屏幕上显示视图时，调用viewDidAppear：方法。

添加，删除或修改视图的大小或位置时，请记住要添加和删除所有应用于这些视图的约束。对视图层次结构进行布局相关的更改会导致UIKit将布局标记为脏。在下一个更新周期内，布局引擎使用当前的布局约束来计算视图的大小和位置，并将这些更改应用于视图层次结构。

### 管理视图布局

当视图的大小和位置更改时，UIKit会更新视图层次结构的布局信息。对于使用“自动布局”配置的视图，UIKit会使用“自动布局”引擎，并使用它根据当前约束来更新布局。UIKit还让其他感兴趣的对象（例如活动的演示控制器）知道布局更改，以便它们可以做出相应的响应。

在布局过程中，UIKit会在几个时间点通知您，以便您可以执行其他与布局有关的任务。在应用布局约束后，可以使用这些通知来修改布局约束或对布局进行最终调整。在布局过程中，UIKit对每个受影响的视图控制器执行以下操作： 1. 根据需要更新视图控制器及其视图的特征集合。 2. 调用视图控制器的viewWillLayoutSubviews方法。 3. 调用当前UIPresentationController对象的containerViewWillLayoutSubviews方法。 4. 调用视图控制器的根视图的layoutSubviews方法。 此方法的默认实现使用可用的约束来计算新的布局信息。然后，该方法遍历视图层次结构，并为每个子视图调用layoutSubviews。 5. 将计算出的布局信息应用于视图。 6. 调用视图控制器的viewDidLayoutSubviews方法。 7. 调用当前UIPresentationController对象的containerViewDidLayoutSubviews方法。

视图控制器可以使用viewWillLayoutSubviews和viewDidLayoutSubviews方法执行可能影响布局过程的其他更新。在布局之前，您可以添加或删除视图，更新视图的大小或位置，更新约束或更新其他与视图相关的属性。布局后，您可以重新加载表数据，更新其他视图的内容或对视图的大小和位置进行最终调整。

以下是一些有效管理布局的技巧：

* 使用自动布局。使用自动版面创建的约束是将内容放置在不同屏幕尺寸上的灵活便捷的方法。
* 利用顶部和底部布局指南。将内容布置到这些指南可确保您的内容始终可见。顶部布局向导的位置会影响状态栏和导航栏的高度。同样，底部布局的引导因素会影响标签栏或工具栏的高度。
* 添加或删除视图时，请记住要更新约束。如果您动态添加或删除视图，请记住要更新相应的约束。
* 在为视图控制器的视图设置动画时，暂时删除约束。使用UIKit Core Animation为视图设置动画时，请删除动画持续时间的约束，并在动画结束时将其重新添加。如果在动画过程中视图的位置或大小发生更改，请记住要更新约束。

### 有效管理内存

尽管您可以决定内存分配的大多数方面，但是下表列出了最有可能分配或取消分配内存的UIViewController方法。大多数释放都涉及删除对对象的强引用。要删除对对象的强引用，请将指向该对象的属性和变量设置为nil。

| 任务                | 方法                      | 讨论                                                                                      |
| ----------------- | ----------------------- | --------------------------------------------------------------------------------------- |
| 分配视图控制器所需的关键数据结构。 | 初始化方法                   | <p>您的自定义初始化方法（无论是命名为init还是其他名称）始终负责将视图控制器对象置于已知的良好状态。<br>这些方法可以分配所需的任何数据结构，以确保正常运行。</p> |
| 分配或加载要在视图中显示的数据。  | viewDidLoad             | <p>使用viewDidLoad方法加载要显示的任何数据对象。<br>在调用此方法时，可以确保您的视图对象已经存在并处于已知的良好状态。</p>                |
| 响应低内存通知。          | didReceiveMemoryWarning | 使用此方法可以取消分配与视图控制器关联的所有非关键对象。尽可能多地释放内存。                                                  |
| 释放视图控制器所需的关键数据结构  | dealloc                 | <p>重写此方法仅是为了执行对视图控制器类的最后一刻清理。<br>系统会自动释放存储在类的实例变量和属性中的对象，因此您无需显式释放那些对象。</p>             |

## 实现容器视图控制器

容器视图控制器是一种将来自多个视图控制器的内容组合到单个用户界面中的方法。容器视图控制器最常用于促进导航并基于现有内容创建新的用户界面类型。UIKit中的容器视图控制器的示例包括UINavigationController，UITabBarController和UISplitViewController，所有这些都有助于在用户界面的不同部分之间进行导航。

### 设计自定义容器视图控制器

几乎在所有方面，容器视图控制器都像其他任何内容视图控制器一样，它可以管理根视图和某些内容。区别在于容器视图控制器从其他视图控制器获取其内容的一部分。它获取的内容仅限于其他视图控制器的视图，这些视图嵌入在自己的视图层次结构中。容器视图控制器设置任何嵌入式视图的大小和位置，但是原始视图控制器仍在管理这些视图内的内容。

在设计自己的容器视图控制器时，请始终了解容器与包含的视图控制器之间的关系。视图控制器的关系可以帮助告知其内容应如何显示在屏幕上以及容器如何在内部对其进行管理。在设计过程中，请问自己以下问题：

* 容器的作用是什么？它的子容器起什么作用？
* 同时显示几个界面？
* 同级视图控制器之间的关系是什么（如果有）？
* 子视图控制器如何添加到容器或从容器中删除？
* 孩子们的大小或位置可以改变吗？这些变化在什么条件下发生？
* 容器是否提供自己的装饰性或导航性视图？
* 容器与其子容器之间需要什么样的通信？除了UIViewController类定义的标准事件之外，容器是否需要向其子项报告特定事件？
* 容器的外观可以用不同的方式配置吗？如果是这样，怎么办？

在定义各种对象的角色之后，容器视图控制器的实现相对简单。UIKit的唯一要求是您在容器视图控制器和任何子视图控制器之间建立正式的父子关系。父子关系可确保子代接收任何相关的系统消息。除此之外，大多数实际工作发生在所包含视图的布局和管理期间，这对于每个容器而言都是不同的。您可以将视图放置在容器内容区域中的任何位置，并根据需要调整这些视图的大小。您还可以将自定义视图添加到视图层次结构，以提供修饰或帮助导航。

### 示例：导航控制器

UINavigationController对象支持在分层数据集中进行导航。导航界面一次显示一个子视图控制器。界面顶部的导航栏显示数据层次结构中的当前位置，并显示一个后退按钮以将其后移一级。向下导航到数据层次结构留给子视图控制器，并且可能涉及使用表或按钮。

视图控制器之间的导航由导航控制器及其子级共同管理。当用户与子视图控制器的按钮或表格行进行交互时，该子请求导航控制器将新的视图控制器推入视图。子级处理新视图控制器内容的配置，而导航控制器管理过渡动画。导航控制器还管理导航栏，该导航栏显示用于退出最上方视图控制器的后退按钮。

下图显示了导航控制器的结构及其视图。内容区域的大部分由最上方的子视图控制器填充，而导航栏仅占据一小部分。

![](https://3323963180-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MSfsKp5x5jHFPTcZRiN%2Fsync%2F5444d0951e8272a792c6579624ab86427b5d1992.png?generation=1612774876847210\&alt=media)

### 示例：拆分视图控制器

UISplitViewController对象以主从方式显示两个视图控制器的内容。在这种布置中，一个视图控制器（主机）的内容确定了另一视图控制器显示哪些详细信息。两个视图控制器的可见性是可配置的，但也受当前环境的控制。在规则的水平环境中，拆分视图控制器可以并排显示两个子视图控制器，也可以隐藏母版并根据需要显示它。在紧凑的环境中，拆分视图控制器一次仅显示一个视图控制器。

下图显示了拆分视图界面的结构及其在规则水平环境中的视图。默认情况下，拆分视图控制器本身仅具有其容器视图。在此示例中，两个子视图并排显示。子视图的大小以及主视图的可见性都是可配置的。

![](https://3323963180-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MSfsKp5x5jHFPTcZRiN%2Fsync%2Fbd0408cdbfec18ab9b9b8633d9fa8c23018a409c.png?generation=1612774876771569\&alt=media)

### 在Interface Builder中配置容器

要在设计时创建父子容器关系，请将一个容器视图对象添加到情节提要场景中，如下图所示。容器视图对象是一个占位符对象，代表子视图控制器的内容。使用该视图调整子级根视图的大小和位置，使其相对于容器中的其他视图。

![](https://3323963180-files.gitbook.io/~/files/v0/b/gitbook-legacy-files/o/assets%2F-MSfsKp5x5jHFPTcZRiN%2Fsync%2Fcda885d0a531400ee7749690dffcd4d446bf5684.png?generation=1612774876867927\&alt=media)

当使用一个或多个容器视图加载视图控制器时，Interface Builder还将加载与那些视图关联的子视图控制器。子级必须与父级同时实例化，以便可以创建适当的父子关系。

如果您不使用Interface Builder来建立父子容器关系，则必须通过将每个子容器添加到容器视图控制器中，以编程方式创建这些关系。

### 实现自定义容器视图控制器

要实现容器视图控制器，必须在视图控制器及其子视图控制器之间建立关系。在尝试管理任何子视图控制器的视图之前，必须先建立这些父子关系。这样做会使UIKit知道您的视图控制器正在管理子级的大小和位置。您可以在Interface Builder中创建这些关系或以编程方式创建它们。以编程方式创建父子关系时，作为视图控制器设置的一部分，您显式添加和删除子视图控制器。

#### 将子视图控制器添加到您的内容

要将子视图控制器以编程方式合并到您的内容中，请执行以下操作在相关视图控制器之间创建父子关系： 1. 调用容器视图控制器的addChildViewController：方法。 此方法告诉UIKit，您的容器视图控制器现在正在管理子视图控制器的视图。 2. 将孩子的根视图添加到容器的视图层次结构中。 在此过程中，请务必记住设置孩子框架的尺寸和位置。 3. 添加任何约束来管理子级根视图的大小和位置。 4. 调用子视图控制器的didMoveToParentViewController：方法。

以下代码显示了容器如何在其容器中嵌入子视图控制器。建立父子关系后，容器将设置其子框架并将子视图添加到其自己的视图层次结构中。设置子视图的框架大小很重要，并确保该视图正确显示在您的容器中。添加视图后，容器将调用子级的didMoveToParentViewController：方法，以使子级视图控制器有机会响应视图所有权的更改。

```
- (void) displayContentController: (UIViewController*) content {
   [self addChildViewController:content];
   content.view.frame = [self frameForContentController];
   [self.view addSubview:self.currentClientView];
   [content didMoveToParentViewController:self];
}
```

在前面的示例中，请注意，您仅调用子级的didMoveToParentViewController：方法。这是因为addChildViewController：方法会为您调用子级的willMoveToParentViewController：方法。您必须自己调用didMoveToParentViewController：方法的原因是，直到将孩子的视图嵌入到容器的视图层次结构中之后，才能调用该方法。

使用自动版式时，在将子级添加到容器的视图层次结构之后，在容器和子级之间设置约束。您的约束条件只会影响孩子的根视图的大小和位置。请勿更改根视图或子级视图层次结构中任何其他视图的内容。

### 移除子视图控制器

要从您的内容中删除子级视图控制器，请执行以下操作以删除视图控制器之间的父子关系： 1. 调用值为nil的孩子的willMoveToParentViewController：方法。 2. 删除您使用子级根视图配置的所有约束。 3. 从容器的视图层次结构中删除子级的根视图。 4. 调用子项的removeFromParentViewController方法，以确定断开父子关系。

删除子视图控制器将永久切断父子之间的关系。仅在不再需要引用子视图控制器时，才删除它。例如，当将一个新的子视图控制器推入导航堆栈时，它不会删除其当前的子视图控制器。仅当它们从堆栈中弹出时，才将其删除。

以下代码显示了如何从其容器中删除子视图控制器。调用值为nil的willMoveToParentViewController：方法将使子视图控制器有机会为更改做准备。removeFromParentViewController方法还会调用子级的didMoveToParentViewController：方法，并将该方法的值传递为nil。将父视图控制器设置为nil可以最终从容器中删除子视图。

```
- (void) hideContentController: (UIViewController*) content {
   [content willMoveToParentViewController:nil];
   [content.view removeFromSuperview];
   [content removeFromParentViewController];
}
```

### 子视图控制器之间的过渡

如果要动画化一个子视图控制器替换另一个子视图控制器的动画，请将子视图控制器的添加和删除合并到过渡动画过程中。在制作动画之前，请确保两个子视图控制器都属于您的内容，但请让当前的孩子知道它即将消失。在动画过程中，将新的子视图移到适当位置，然后删除旧的子视图。动画完成时，完成子视图控制器的移除。

以下代码显示了如何使用过渡动画将一个子视图控制器交换为另一个子视图控制器的示例。在此示例中，将新视图控制器动画化为现有子视图控制器当前占据的矩形，该矩形被移出屏幕。动画结束后，完成块将从容器中删除子视图控制器。在此示例中，transitionFromViewController：toViewController：duration：options：animations：completion：方法会自动更新容器的视图层次结构，因此您无需自己添加和删除视图。

```
- (void)cycleFromViewController: (UIViewController*) oldVC
               toViewController: (UIViewController*) newVC {
   // Prepare the two view controllers for the change.
   [oldVC willMoveToParentViewController:nil];
   [self addChildViewController:newVC];

   // Get the start frame of the new view controller and the end frame
   // for the old view controller. Both rectangles are offscreen.
   newVC.view.frame = [self newViewStartFrame];
   CGRect endFrame = [self oldViewEndFrame];

   // Queue up the transition animation.
   [self transitionFromViewController: oldVC toViewController: newVC
        duration: 0.25 options:0
        animations:^{
            // Animate the views to their final positions.
            newVC.view.frame = oldVC.view.frame;
            oldVC.view.frame = endFrame;
        }
        completion:^(BOOL finished) {
           // Remove the old view controller and send the final
           // notification to the new view controller.
           [oldVC removeFromParentViewController];
           [newVC didMoveToParentViewController:self];
        }];
}
```

### 管理子控制器外观更新

将子项添加到容器后，该容器会自动将与外观相关的消息转发给子项。这通常是您想要的行为，因为它可以确保正确发送所有事件。但是，有时默认行为可能会以对您的容器没有意义的顺序发送这些事件。例如，如果多个子项同时更改其视图状态，则可能需要合并更改，以便外观回调全部以更合乎逻辑的顺序同时发生。

要承担外观回调的责任，请在容器视图控制器中重写shouldAutomaticallyForwardAppearanceMethods方法并返回NO，如以下代码所示。返回NO将使UIKit知道您的容器视图控制器将其外观更改通知其子级。

```
- (BOOL) shouldAutomaticallyForwardAppearanceMethods {
    return NO;
}
```

发生外观转换时，请适当地调用孩子的beginAppearanceTransition：animated：或endAppearanceTransition方法。例如，如果您的容器有一个由child属性引用的孩子，那么您的容器会将这些消息转发给孩子，如以下代码所示。

```
-(void) viewWillAppear:(BOOL)animated {
    [self.child beginAppearanceTransition: YES animated: animated];
}

-(void) viewDidAppear:(BOOL)animated {
    [self.child endAppearanceTransition];
}

-(void) viewWillDisappear:(BOOL)animated {
    [self.child beginAppearanceTransition: NO animated: animated];
}

-(void) viewDidDisappear:(BOOL)animated {
    [self.child endAppearanceTransition];
}
```

### 有关构建容器视图控制器的建议

设计，开发和测试新的容器视图控制器需要花费时间。尽管各个行为很简单，但作为一个整体，控制器可能非常复杂。在实现自己的容器类时，请考虑以下提示：

* 仅访问子视图控制器的根视图。容器只能访问每个孩子的根视图，即孩子的view属性返回的视图。它绝对不能访问孩子的其他任何视图。
* 子视图控制器应该对其容器了解最少。子视图控制器应专注于自己的内容。如果容器允许其行为受到孩子的影响，则应使用委托设计模式来管理这些交互。
* 首先使用常规视图设计容器。使用常规视图（而不是子视图控制器的视图）使您有机会在简化的环境中测试布局约束和动画过渡。当常规视图按预期工作时，将它们换成子视图控制器的视图。

### 将控制委派给子视图控制器

容器视图控制器可以将其外观的某些方面委派给其一个或多个子代。您可以通过以下方式委派控制：

* 让一个子视图控制器确定状态栏样式。要将状态栏外观委派给孩子，请在容器视图控制器中重写childViewControllerForStatusBarStyle和childViewControllerForStatusBarHidden方法中的一个或两个。
* 让子视图控制器指定自己喜欢的尺寸。具有灵活布局的容器可以使用子代自己的preferredContentSize属性来帮助确定子代的大小。
