AddChildViewController 实际上是做什么的?

我只是第一次涉足 iOS 开发,我必须做的第一件事就是实现一个 自定义容器视图控制器(我们称之为 SideBarViewController) ,它可以替换几个可能显示的子视图控制器中的哪一个,几乎完全像一个标准的 标签条控制器。(它基本上是一个 标签条控制器,但是有一个可以隐藏的侧面菜单,而不是一个标签栏。)

根据 Apple 文档中的说明,每当向容器中添加一个子 ViewController 时,我都会调用 addChildViewController。我用于交换 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在这里做什么,我意识到我不知道。除了在 .childViewControllers阵列中添加新的 ViewController之外,它似乎对任何东西都没有影响。从子控制器视图到我在故事板上设置的子控制器的操作和出口仍然可以正常工作,即使我从未调用 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;
}

... 那么我的应用程序还能正常工作,就我所知!

苹果的文档没有对 addChildViewController做了什么或者为什么我们应该称之为 addChildViewController做了很多解释。关于该方法的作用或为什么应该在 UIViewController类别参考的章节中使用该方法的相关说明的整个范围目前是:

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

同一页的前面还有一段:

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

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

AddChildViewController:
控制器
控制器:
控制器:

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

在苹果文档的“自定义容器视图控制器”部分中的自定义容器视图控制器的例子都调用了这个方法,所以我假设它除了将子 ViewController 弹出到一个数组中之外还有其他重要用途,但是我不知道这个用途是什么。这个方法是做什么的,我为什么要调用它?

46497 次浏览

-[UIViewController addChildViewController:] only adds the passed in view controller in an array of viewControllers that a viewController (the parent) wants to keep reference of. You should actually add those viewController's views on screen yourself by adding them as a subviews of another view (e.g. the parentViewController's view). There's also a convenience object in Interface Builder to use childrenViewControllers in Storyboards.

Previously, to keep reference of other viewControllers of which you used the views of, you had to keep manual reference of them in @properties. Having a build-in property like childViewControllers and consequently parentViewController is a convenient way to manage such interactions and build composed viewControllers like the UISplitViewController that you find on iPad apps.

Moreover, childrenViewControllers also automatically receive all the system events that the parent receives: -viewWillAppear, -viewWillDisappear, etc. Previously you should have called this methods manually on your "childrenViewControllers".

That's it.

I think an example is worth a thousand words.

I was working on a library app and wanted to show a nice notepad view that appears when the user wants to add a note.

enter image description here

After trying some solutions, I ended up inventing my own custom solution to show the notepad. So when I want to show the notepad, I create a new instance of NotepadViewController and add its root view as a subview to the main view. So far so good.

Then I noticed that the notepad image is partially hidden under the keyboard in landscape mode.

enter image description here

So I wanted to change the notepad image and shift it up. And to do so, I wrote the proper code in willAnimateRotationToInterfaceOrientation:duration: method, but when I ran the app nothing happened! And after debugging I noticed that none of UIViewController's rotation methods is actually called in NotepadViewController. Only those methods in the main view controller are being called.

To solve this, I needed to call all the methods from NotepadViewController manually when they're called in the main view controller. This will soon make things complicated and create an extra dependency between unrelated components in the app.

That was in the past, before the concept of child view controllers is introduced. But now, you only need to addChildViewController to the main view controller and everything will just work as expected without any more manual work.

Edit: There are two categories of events that are forwarded to child view controllers:

1- Appearance Methods:

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

2- Rotation Methods:

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

You can also control what event categories you want to be forwarded automatically by overriding shouldAutomaticallyForwardRotationMethods and shouldAutomaticallyForwardAppearanceMethods.

I was wondering about this question too. I watched Session 102 of the WWDC 2011 videos and Mr. View Controller, Bruce D. Nilo, said this:

viewWillAppear:, viewDidAppear:, etc have nothing to do with addChildViewController:. All that addChildViewController: does is to say "This view controller is a child of that one" and it has nothing to do with view appearance. When they get called is associated with when views move in and out of the window hierarchy.

So it seems that the call to addChildViewController: does very little. The side effects of the call are the important part. They come from the parentViewController and childViewControllers relationships. Here are some of the side effects that I know:

  • Forwarding appearance methods to child view controllers
  • Forwarding rotation methods
  • (Possibly) forwarding memory warnings
  • Avoiding inconsistent VC hierarchies, especially in transitionFromViewController:toViewController:… where both VCs need to have the same parent
  • Allowing custom container view controllers to take part in State Preservation and Restoration
  • Taking part in the responder chain
  • Hooking up the navigationController, tabBarController, etc properties

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.