ChatGPT解决这个技术问题 Extra ChatGPT

addChildViewController 实际上做了什么?

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

根据 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;
}

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

Apple 文档并没有对 addChildViewController 的作用或我们应该调用它的原因提供太多说明。目前,在 UIViewController Class Reference 的部分中对该方法的作用或为什么应该使用它的相关描述的全部范围是:

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

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

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

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

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

Apple 的 2011 WWDC 视频页面有一个关于此主题的精彩 会话(“实施 UIViewController 包含”)。

H
Hejazi

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

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

https://i.stack.imgur.com/0iAFB.png

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

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

https://i.stack.imgur.com/IN72v.png

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

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

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

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

1-外观方法:

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

2- 旋转方法:

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

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


从文档中并在进行快速测试后,我认为只有在您 addChildViewController 到父控制器时才会转发任何其他事件。
希望它自动转发 viewWillLayoutSubviews
p
pkamb

我也想知道这个问题。我看了 Session 102 of the WWDC 2011 个视频,Bruce D. Nilo 的 View Controller 先生说:

viewWillAppear:, viewDidAppear: 等与 addChildViewController: 无关。 addChildViewController: 所做的只是说“这个视图控制器是那个的孩子”,它与视图外观无关。它们何时被调用与视图何时移入和移出窗口层次结构相关联。

因此,对 addChildViewController: 的调用似乎作用很小。通话的副作用是重要的部分。它们来自 parentViewControllerchildViewControllers 关系。以下是我知道的一些副作用:

将外观方法转发给子视图控制器

转发轮换方法

(可能)转发内存警告

避免不一致的 VC 层次结构,尤其是在 transitionFromViewController:toViewController:... 两个 VC 需要具有相同父级的情况下

允许自定义容器视图控制器参与状态保存和恢复

参与响应者链

连接 navigationController、tabBarController 等属性


+1 响应者链。如果您想在子 UIViewController 拥有的子视图上接收触摸事件,则需要 addChildViewController
G
Gianluca Tranchedone

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

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

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

而已。


你认为这就是它所做的一切的基础是什么?另外,您能否提供孩子收到的“系统事件”列表? Google 搜索 iOS "system events" 并没有太多问题;它似乎不是Apple使用的术语?
它基本上是一种方便的方法,允许您将 View Controller B 的视图添加为 View Controller A 的子视图,但仍然让 View Controller B 管理其视图。为了使其正常工作,您需要确保 View Controller B 正在获取系统事件(读取 UIViewControllerDelegate 回调)。 'addChildViewController' 为您连接了这一点,以节省您手动传递所有内容的工作量。
R
Rob

addChildViewController 实际上做了什么?

这是视图包含的第一步,我们通过该过程使视图层次结构与视图控制器层次结构保持同步,对于那些我们有一个子视图将其逻辑封装在它自己的视图控制器中的情况(以简化父视图控制器,以启用具有自己逻辑的可重用子视图等)。

因此,addChildViewController 将子视图控制器添加到 childViewControllers 的数组中,它跟踪子视图、帮助它们获取所有视图事件、为您保留对子视图的强引用等。

但请注意,addChildViewController 只是第一步。您还必须调用 didMoveToParentViewController

- (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
}

顺便说一句,请注意调用的顺序,其中调用它们的顺序很重要。例如,在添加时,您按此顺序调用 addChildViewControlleraddSubviewdidMoveToParentViewController。请注意,正如 didMoveToParentViewController the documentation 所说:

如果你正在实现自己的容器视图控制器,它必须在转换到新控制器后调用子视图控制器的 didMoveToParentViewController: 方法,或者如果没有转换,则在调用 addChildViewController: 方法后立即调用。

而且,如果您想知道为什么我们在这种情况下也不调用 willMoveToParentViewController,那是因为,正如 the documentation 所说,addChildViewController 会为您这样做:

当您的自定义容器调用 addChildViewController: 方法时,它会在添加视图控制器之前自动调用要添加为子视图控制器的 willMoveToParentViewController: 方法。

同样,删除时,您调用 willMoveToParentViewControllerremoveFromSuperviewremoveFromParentViewController。正如 willMoveToParentViewControllerthe documentation 所说:

如果你正在实现自己的容器视图控制器,它必须在调用 removeFromParentViewController 方法之前调用子视图控制器的 willMoveToParentViewController: 方法,传入一个父值 nil。

而且,如果您想知道为什么我们在移除子项时不调用 didMoveToParentViewController,那是因为,正如 the documentation 所说,removeFromParentViewController 会为您执行此操作:

removeFromParentViewController 方法在移除子视图后会自动调用子视图控制器的 didMoveToParentViewController: 方法。

仅供参考,如果对子视图的删除进行动画处理,请将对 removeFromParentViewController 的调用放入动画完成处理程序中。

但是,如果您执行正确的收容调用序列,如上所述,那么孩子将收到所有适当的与视图相关的事件。

有关详细信息(特别是为什么这些 willMoveToParentViewControllerdidMoveToParentViewController 调用如此重要),请参阅 WWDC 2011 视频 Implementing UIViewController Containment。另请参阅 UIViewController documentation实现容器视图控制器部分。

作为一个小观察,确保当您将子视图添加为子视图时,引用父视图控制器视图的 bounds,而不是 frame。父视图的 frame其父视图的坐标系中。 bounds 位于其自己的坐标系中。

当父视图占据全屏时,您可能不会注意到差异,但是一旦您在父视图没有占据全屏的情况下使用此功能,您就会开始遇到帧错位。为孩子设置坐标时始终使用 bounds。 (或者使用约束,这会让你完全摆脱这种愚蠢。)

也许不用说,如果您只想在实例化父对象时添加子对象,则可以完全在情节提要中完成视图控制器包含,而无需任何这些 add/removewillMove/didMove 调用.只需使用“容器视图”并使用 prepareForSegue 在初始化期间传递孩子需要的任何数据。

https://i.stack.imgur.com/6raG3.gif

例如,如果父级有一个名为 bar 的属性,而您想更新子级中名为 baz 的属性:

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

        destination.baz = self.bar;
    }
}

现在,如果您想以编程方式添加/删除子项,请按照上述说明使用。但是情节提要“容器视图”可以用很少的代码处理简单场景的所有视图包含调用。