104

我只是第一次涉足 iOS 开发,我必须做的第一件事就是实现一个自定义容器视图控制器- 让我们调用它SideBarViewController- 交换几个可能的子视图控制器中的哪一个显示,几乎与标准的Tab Bar Controller完全一样。(它几乎是一个标签栏控制器,但有一个可隐藏的侧边菜单而不是标签栏。)

根据 Apple 文档中的说明,addChildViewController每当我将子 ViewController 添加到我的容器时,我都会调用。我用于换出当前子视图控制器的代码如下所示SideBarViewController

- (void)showViewController:(UIViewController *)newViewController {
    UIViewController* oldViewController = [self.childViewControllers 
                                           objectAtIndex:0];
    
    [oldViewController removeFromParentViewController];
    [oldViewController.view removeFromSuperview];
    
    newViewController.view.frame = CGRectMake(
        0, 0, self.view.frame.size.width, self.view.frame.size.height
    );
    [self addChildViewController: newViewController];
    [self.view addSubview: newViewController.view];
}

然后我开始试图弄清楚addChildViewController这里是做什么的,我意识到我不知道。ViewController除了在数组中粘贴新的.childViewControllers,它似乎对任何东西都没有影响。即使我从不调用addChildViewController,从子控制器的视图到子控制器的操作和出口仍然可以正常工作,而且我无法想象它还会影响什么。

确实,如果我将代码重写为不调用addChildViewController,而是看起来像这样......

- (void)showViewController:(UIViewController *)newViewController {

    // Get the current child from a member variable of `SideBarViewController`
    UIViewController* oldViewController = currentChildViewController;

    [oldViewController.view removeFromSuperview];

    newViewController.view.frame = CGRectMake(
        0, 0, self.view.frame.size.width, self.view.frame.size.height
    );
    [self.view addSubview: newViewController.view];

    currentChildViewController = newViewController;
}

...那么我的应用程序仍然可以完美运行,据我所知!

Apple 文档并没有详细说明它是做什么addChildViewController的,或者我们为什么要这么称呼它。UIViewController目前,在Class Reference的部分中,对该方法的作用或为什么应该使用它的相关描述的全部范围是:

将给定的视图控制器添加为子视图。...此方法仅旨在由自定义容器视图控制器的实现调用。如果你重写这个方法,你必须在你的实现中调用 super。

在同一页的前面还有这段:

在将子视图控制器添加到视图层次结构之前,您的容器视图控制器必须将子视图控制器与其自身相关联。这允许 iOS 正确地将事件路由到子视图控制器和这些控制器管理的视图。同样,在它从其视图层次结构中删除子视图的根视图后,它应该断开该子视图控制器与自身的连接。要建立或破坏这些关联,您的容器会调用基类定义的特定方法。这些方法不打算由您的容器类的客户端调用;它们只能由您的容器实现使用,以提供预期的遏制行为。

以下是您可能需要调用的基本方法:

addChildViewController:
removeFromParentViewController
willMoveToParentViewController:
didMoveToParentViewController:

但它没有提供任何线索来说明它所谈论的“事件”或“预期的收容行为”是什么,或者为什么(甚至何时)调用这些方法是“必要的”。

Apple 文档的“自定义容器视图控制器”部分中的自定义容器视图控制器示例都调用了这个方法,所以我认为它除了将子 ViewController 弹出到一个数组之外还有一些重要的目的,但我想不通出那个目的是什么。这个方法有什么作用,我为什么要调用它?

4

4 回答 4

111

我认为一个例子值一千字。

我正在开发一个图书馆应用程序,并希望在用户想要添加笔记时显示一个漂亮的记事本视图。

在此处输入图像描述

在尝试了一些解决方案后,我最终发明了自己的自定义解决方案来显示记事本。因此,当我想显示记事本时,我创建了一个新实例NotepadViewController并将其根视图作为子视图添加到主视图中。到现在为止还挺好。

然后我注意到在横向模式下,记事本图像部分隐藏在键盘下方。

在此处输入图像描述

所以我想更改记事本图像并将其向上移动。为此,我在willAnimateRotationToInterfaceOrientation:duration:方法中编写了正确的代码,但是当我运行应用程序时,什么也没发生!调试后我注意到UIViewController实际上没有调用任何旋转方法NotepadViewController。只有主视图控制器中的那些方法被调用。

为了解决这个问题,我需要NotepadViewController在主视图控制器中调用所有方法时手动调用它们。这很快就会使事情变得复杂,并在应用程序中不相关的组件之间产生额外的依赖关系。

那是过去,在引入子视图控制器的概念之前。但是现在,您只需要到 addChildViewController主视图控制器,一切都会按预期工作,无需任何手动操作。

编辑: 有两类事件被转发到子视图控制器:

1-外观方法:

- viewWillAppear:
- viewDidAppear:
- viewWillDisappear:
- viewDidDisappear:

2- 旋转方法:

- willRotateToInterfaceOrientation:duration:
- willAnimateRotationToInterfaceOrientation:duration:
- didRotateFromInterfaceOrientation:

shouldAutomaticallyForwardRotationMethods您还可以通过覆盖和来控制要自动转发的事件类别shouldAutomaticallyForwardAppearanceMethods

于 2013-06-27T00:54:01.187 回答
97
于 2013-08-26T21:34:03.837 回答
10

-[UIViewController addChildViewController:]仅将传入的视图控制器添加到视图控制器(父级)想要保留引用的视图控制器数组中。实际上,您应该通过将它们添加为另一个视图的子视图(例如 parentViewController 的视图)来自己在屏幕上添加这些 viewController 的视图。Interface Builder 中还有一个便利对象,可以在 Storyboard 中使用 childrenViewController。

以前,要保留对您使用其视图的其他 viewController 的引用,您必须在 @properties 中保留对它们的手动引用。childViewControllers拥有类似and的内置属性parentViewController是管理此类交互和构建组合视图控制器(如您在 iPad 应用程序上找到的 UISplitViewController)的便捷方式。

此外,childrenViewControllers 还会自动接收父级接收到的所有系统事件:-viewWillAppear、-viewWillDisappear 等。以前您应该在“childrenViewControllers”上手动调用此方法。

就是这样。

于 2013-06-19T13:16:07.357 回答
0

What does addChildViewController actually do?

It is the first step of view containment, a process by which we keep the view hierarchy in sync with the view controller hierarchy, for those cases where we have a subview that has encapsulated its logic in its own view controller (to simplify the parent view controller, to enable a reusable child view with its own logic, etc.).

So, addChildViewController adds the child view controller to an array of childViewControllers, which keeps track of the children, facilitates them getting all the view events, keeps a strong reference to the child for you, etc.

But note, addChildViewController is only the first step. You also have to call didMoveToParentViewController, too:

- (void)showViewController:(UIViewController *)newViewController {
    UIViewController* oldViewController = [self.childViewControllers objectAtIndex:0];

    [oldViewController willMoveToParentViewController:nil];   // tell it you are going to remove the child
    [oldViewController.view removeFromSuperview];             // remove view
    [oldViewController removeFromParentViewController];       // tell it you have removed child; this calls `didMoveToParentViewController` for you

    newViewController.view.frame = self.view.bounds;          // use `bounds`, not `frame`
    newViewController.view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;  // be explicit regarding resizing mask if setting `frame`

    [self addChildViewController:newViewController];          // tell it that you are going to add a child; this calls `willMoveToParentViewController` for you
    [self.view addSubview:newViewController.view];            // add the view
    [newViewController didMoveToParentViewController:self];   // tell it that you are done
}

As an aside, please note the sequence of calls, in which the order that you call these is important. When adding, for example, you call addChildViewController, addSubview, and didMoveToParentViewController, in that order. Note, as the documentation for didMoveToParentViewController says:

If you are implementing your own container view controller, it must call the didMoveToParentViewController: method of the child view controller after the transition to the new controller is complete or, if there is no transition, immediately after calling the addChildViewController: method.

And, if you are wondering why we don't call willMoveToParentViewController in this case, too, it is because, as the documentation says, addChildViewController does that for you:

When your custom container calls the addChildViewController: method, it automatically calls the willMoveToParentViewController: method of the view controller to be added as a child before adding it.

Likewise, when removing, you call willMoveToParentViewController, removeFromSuperview, and removeFromParentViewController. As the documentation for willMoveToParentViewController says:

If you are implementing your own container view controller, it must call the willMoveToParentViewController: method of the child view controller before calling the removeFromParentViewController method, passing in a parent value of nil.

And, again, if you are wondering why we don't call didMoveToParentViewController when removing the child, that is because, as the documentation says, removeFromParentViewController does that for you:

The removeFromParentViewController method automatically calls the didMoveToParentViewController: method of the child view controller after it removes the child.

FYI, if animating the removal of the subview, put the call to removeFromParentViewController in the animation completion handler.

But if you perform the correct sequence of containment calls, outlined above, then the child will receive all of the appropriate view-related events.

For more information (in particular, why these willMoveToParentViewController and didMoveToParentViewController calls are so important), see WWDC 2011 video Implementing UIViewController Containment. Also see the Implementing a Container View Controller section of the UIViewController documentation.


As a minor observation, make sure that when you are adding the child’s view as a subview, reference the bounds of the parent view controller’s view, not the frame. The frame of the parent’s view is in the coordinate system of its superview. The bounds is in its own coordinate system.

You might not notice the difference when the parent view occupies the full screen, but as soon as you employ this in a scenario where the parent’s view doesn't happen to take up the full screen, you will start to encounter frame misalignment. Always use bounds when setting up coordinates for children. (Or use constraints, which gets you out of that silliness, altogether.)


Perhaps needless to say, if you want to just add the child when the parent is instantiated, one can do view controller containment entirely in storyboards without any of these add/remove and willMove/didMove calls at all. Just use “Container View” and pass whatever data is needed by the child during initialization using prepareForSegue.

enter image description here

For example, if the parent had a property called bar and you wanted to update a property called baz in the child:

- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
    if ([segue.destinationViewController isKindOfClass:[ChildViewController class]]) {
        ChildViewController *destination = segue.destinationViewController;

        destination.baz = self.bar;
    }
}

Now, if you want to programmatically add/remove children, then use as outlined above. But storyboard “Container View” can handle all the view containment calls for simple scenarios with very little code.

于 2021-08-09T17:00:59.640 回答