addChildViewController后 Childvc viewWillAppear 不调用的问题

【一】认识不全的笔记

设置
[childVC beginAppearanceTransition:YES animated:YES]
即可

移除的时候要这样 :

[childController beginAppearanceTransition:NO animated:YES];
[childController.view removeFromSuperview];
[childController endAppearanceTransition];
viewWillDisappear和viewDidDisappear才会被调用

参见:
http://www.jianshu.com/p/7587cce3ede0

http://blog.csdn.net/jlstmacblog/article/details/52414301

【二】认识补全

详细参考如下:

addChildViewController 与 addSubview 调用顺序

Container View Controller

iOS 实际开发中,为了方便管理视图,我们经常会把一个 VC (ViewController) 作为容器 VC,其他子视图的 VC 作为容器 VC 的子 VC。同时 view 也作为容器 View 的子 view (subview)。苹果官方文档称为 Container View Controller。需要注意的是,在调用 addChildViewController 和 addSubview 时需要注意调用顺序,官方文档建议:

1、添加子视图的时候,代码调用顺序:

- (void) displayContentController: (UIViewController*) content {
    // 1. 添加 子vc,调用 addChildViewController
   [self addChildViewController:content];
    // 2. 设置 子视图的 frame
   content.view.frame = [self frameForContentController];
    // 3. 添加子视图, 调用 addSubview
   [self.view addSubview:self.currentClientView];
    // 4. 调用 didMoveToParentViewController
   [content didMoveToParentViewController:self];
}

需要注意的是,添加的时候,不需要显式的调用:willMoveToParentViewController,因为 addChildViewController 会帮你调用该方法。

2、移除子视图的时候,代码调用顺序:

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

与添加的时候相反,这里不需要显示的调用 didMoveToParentViewController,removeFromParentViewController 内部会调用 didMoveToParentViewController ,并且把传入 nil 参数。

addChildViewController 的重要性


在向 container view controller 中添加 subview 的时候,往往会同时添加子 vc 关系。除了保证响应链的正常传递外,子 vc 的视图生命周期函数也会跟随父 vc 被触发。例如我们在 viewDidLoad 函数中添加了子 vc 和子 view:

- (void)viewDidLoad {
    [super viewDidLoad];

    TTViewController *ttvc = [TTViewController new];
    [self addChildViewController:ttvc];
    [self.view addSubview:ttvc.view];
    [ttvc didMoveToParentViewController:self];
}

虽然在 viewDidLoad 中添加了子 view (此时容器视图的 viewwillAppear 和 viewDidAppear 都没有被触发),但是随后容器 vc 的生命周期方法(viewWillAppear)被触发的时候,容器 vc 会同时遍历自己的 childVC 来同步 childVC 的视图生命周期方法,这一点可以通过观察调用栈看到:

viewDidAppear

(viewWillAppear 方法的调用栈类似上图 ) 所以 addChildViewController 后,上图的 [__NSSingleObjectArrayI enumerateObjectsWithOptions:usingBlock:] 才能产生有效调用。

通过上边的调用栈,大概能看出,视图的 viewDidAppear 的调用,底层是来自 GraphicsServices 库的 GSEventRunModal 的调用,GSEventRunModal 的调用触发 Runloop 回调,来通知视图处理生命周期。所以理论上 viewWillAppear 和 viewDidAppear 的调用都是在不同的 Runloop 上进行的。

viewDidAppear 两次调用场景


实际开发中,当我们通过 addChildViewController 与 addSubview 来添加子视图的时候,存在 viewDidAppear 两次被调用的情况。

产生两次 viewDidAppear 调用的原因,一次是来自 addChildViewController 后,视图跟随容器视图的生命周期方法被调用,另一次触发是来自 addSubview 的调用,并且时机是间于容器视图的 viewWillAppear 和 viewDidAppear 之间。

同时代码顺序是先调用 addSubview 后调用 addChildViewController (猜测 addSubview 内部做了是否触发 viewWillAppear…等方法的判断)。

所以下列代码基本上都会触发子 vc 的 viewDidAppear 两次调用:

- (void)viewDidLoad {
    [super viewDidLoad];

    // 1、dispatch 到下次 runloop
    dispatch_async(dispatch_get_main_queue(), ^{
        [self addSubViewController];
    });

    // 2、perform 到下一次 runloop 
    [self performSelectorOnMainThread:@selector(addSubViewController) withObject:nil waitUntilDone:NO];

    // 3、延迟 0.1 ~ 0.4 秒之间执行 
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.26 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self addSubViewController]; 
    });
}

- (void)addSubViewController {
     TTViewController *ttvc = [TTViewController new];
     // 注意这里先调用的是 addSubview
     [self.view addSubview:ttvc.view];
     [self addChildViewController:ttvc];
     [ttvc didMoveToParentViewController:self];
}

上述代码经过测试,都会触发 viewDidAppear 的两次调用,其中第一次是 addSubview 触发的 (在容器视图的 viewDidAppear 前调用),第二次是容器视图的 viewDidAppear 触发同步给了 childVC 触发的 (上边已经分析过)。

实际开发中,场景更多的可能是网络请求回调的时间刚好是 0.1~0.4 s 之间。而此时刚好执行了 addSubview 操作。

要解决 viewDidAppear 正常的被调用一次,只需要改变调用顺序,即文章开头建议的先 addChildViewController 后调用 addSubvew。

addSubview 与 viewWillAppear / viewDidAppear


为了研究 addSubview 的内部逻辑,我们先抛开 addChildViewController 方法不谈。可以断定的是,addSubview 方法内部一定做了是否触发触发 viewWillAppear 的判断。来看下列代码:

- (void)viewDidLoad {
    [super viewDidLoad];

    TTViewController *ttvc = [TTViewController new];
    [self.view addSubview:ttvc.view];
}

上面代码不会触发子视图的生命周期方法,原因是此时父视图 (self.view) 没有被渲染到屏幕上,所以子视图也不会立刻显示,addSubview 也只能放弃视图的生命周期方法调用。同样的,在 viewWillAppear 中调用,会得到相同的结果。

在 viewDidAppear 视图 addSubview :

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    TTViewController *ttvc = [TTViewController new];
    [self.view addSubview:ttvc.view];
}

此时,子视图 viewWillAppear / viewDidAppear 都会被正常调用, viewWillAppear 断点调用栈如下:

viewWillAppear

可以看到 addSubview 内部会调用 vc 的viewWillMoveToWindow ,从而立刻触发 viewWillAppear 方法。

在看 viewDidAppear 调用栈:
请添加图片描述
可以看到 UIViewController _executeAfterAppearanceBlock 调用后,调用了 vc 的内部方法 viewDidMoveToWindow...,然后触发 viewDidAppear。底层是一个 observer 的 runloop 回调,所以只能猜测,addSubview 方法注册了 observer 的 runloop 事件,等待 Core Animation 视图渲染完成。

所以理论上,如果我们在父视图生命周期执行完成后(viewDidAppear 之后) addSubview ,那么 addSubview 会自己处理生命周期方法,但是尽管如此,我们还是要在调用 addSubview 前加上 addChildViewController 调用,来保证视图的其他处理正常(例如 viewWillDisAppear 等跟随父 vc 的触发)。

通过分析我们也大概能分析出 addSubview 处理的逻辑:


  1. addSubview 的时候,父视图的生命周期函数还没有触发,那么此时自己肯定不能被显示在 window 上,所以不处理任何视图周期函数;此时视图生命周期函数的正常触发,就全靠 addChildViewController 来维持了。

  2. addSubview 的时候,父视图的生命周期函数已经执行完成(viewDidAppear 已经执行完),自己可以立刻显示到 window 上,此时 addSubview 内部会立刻触发 viewWillAppear,同时注册 source0 的 runloop 事件,等待 GraphicsServices 的视图渲染完成的回调,触发 viewDidAppear。

  3. addSubview 的时候,父视图的生命周期函数间于 viewWillAppear 和 viewDidAppear 之间,此时立刻触发 viewWillAppear ,viewDidAppear 则不用处理,跟随父 vc 的调用触发,所以先调用 addChildViewController 的重要性就在这里。

手动控制子视图生命周期


如果想自己手动通过代码严格控制视图生命周期,可以通过:

- (BOOL) shouldAutomaticallyForwardAppearanceMethods {
    return NO;
}

来限制视图生命周期方法的自动触发,但同时要自己管理生命周期:

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

参考文档


https://www.xiaobotalk.com/2019/05/addsubview/
1、苹果开发文档 addchildviewcontroller
2、苹果开发文档 beginappearancetransition
3、苹果开发文档 ImplementingaContainerViewController

【三】认识加深

addChildViewController–控制器包控制器解耦

1.相关属性和方法
@property(nonatomic,readonly) NSArray *childViewControllers
- (void)addChildViewController:(UIViewController *)childController;
- (void)removeFromParentViewController;
- (void)transitionFromViewController::::::;
- (void)willMoveToParentViewController:(UIViewController *)parent
- (void)didMoveToParentViewController:(UIViewController *)parent
2.关于willMoveToParentViewController方法和didMoveToParentViewController方法的使用

这两个方法用在子试图控制器交换的时候调用!即调用transitionFromViewController 方法时,调用。

当调用willMoveToParentViewController方法或didMoveToParentViewController方法时,要注意他们的参数使用:

当某个子视图控制器将从父视图控制器中删除时,parent参数为nil。
即:[将被删除的子试图控制器 willMoveToParentViewController:nil];
当某个子试图控制器将加入到父视图控制器时,parent参数为父视图控制器。
即:[将被加入的子视图控制器 didMoveToParentViewController:父视图控制器];

3.无需调用[childVc willMoveToParentViewController:parent]方法。

因为我们调用[parent addChildViewController:childVc]时,已经默认调用了。
只需要在transitionFromViewController方法后,调用[子视图控制器didMoveToParentViewController:父视图控制器];

4.无需调用[childVc didMoveToParentViewController:parent]方法。

因为我们调用[childVc removeFromParentViewController]时,已经默认调用了。
只需要在transitionFromViewController方法之前调用:[childVc willMoveToParentViewController:nil]。

5.范例
//在parent view controller 中添加 child view controller
FirstViewController *firstViewController=[[FirstViewController alloc] initWithNibName:@"FirstViewController" bundle:nil];
[self addChildViewController:firstViewController];

SecondViewController *secondViewController=[[SecondViewController alloc] initWithNibName:@"SecondViewController" bundle:nil];
[self addChildViewController:secondViewController];

ThirdViewController *thirdViewController=[[ThirdViewController alloc] initWithNibName:@"ThirdViewController" bundle:nil];
[self addChildViewController:thirdViewController];
thirdViewController.view.frame = self.view.bounds;
[self.view  addSubview:thirdViewController.view];

// addChildViewController会调用[child willMoveToParentViewController:self] ,但是不会调用didMoveToParentViewController,所以需要显示调用
[thirdViewController didMoveToParentViewController:self];
currentViewController=thirdViewController;

//切换child view controller
[self transitionFromViewController:currentViewController toViewController:firstViewController duration:4 options:UIViewAnimationOptionTransitionFlipFromLeft animations:^{
            }  completion:^(BOOL finished) {
               //......
            }];
currentViewController=firstViewController;

//移除child view controller
//removeFromParentViewController在移除child前不会调用[self willMoveToParentViewController:nil] ,所以需要显示调用
[currentViewController willMoveToParentViewController:nil];
[currentViewController.view removeFromSuperview];
[currentViewController removeFromParentViewController];

先 addChildViewController 后调用 addSubview,使子控制器的viewDidAppear 只被调用一次

如果先调用 addSubview 后调用 addChildViewController,会产生两次子控制器 viewDidAppear 的调用

一次是来自 addChildViewController 后,视图跟随容器视图的生命周期方法被调用,另一次触发是来自 addSubview 的调用,并且时机是间于容器视图的 viewWillAppear 和 viewDidAppear 之间。 (猜测 addSubview 内部做了是否触发 viewWillAppear…等方法的判断)。
[参考来源: addChildViewController 与 addSubview 调用顺序]
(https://www.xiaobotalk.com/archives/470)

6.应用实例

根据标签切换页面应用实例链接:https://www.jianshu.com/p/913648c9957c

参考:https://juejin.cn/post/6844903894972104718#heading-5

transitionFromViewController:toViewController:duration:options:animations:completion:
官方文档 如下

Summary

Transitions between two of the view controller's child view controllers.
Declaration

- (void)transitionFromViewController:(UIViewController *)fromViewController toViewController:(UIViewController *)toViewController duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion;

Discussion
This method adds the second view controller's view to the view hierarchy and then performs the animations defined in your animations block. After the animation completes, it removes the first view controller's view from the view hierarchy.
This method is only intended to be called by an implementation of a custom container view controller. If you override this method, you must call super in your implementation.
Parameters

/*
译:这个方法会添加第二个vc的view到视图层级中,然后执行在动画block中定义的动画。待动画完成,它从视图层级中移除第一个vc的view。
这个方法目的只在于被一个自定义的容器控制器的实现来调用。如果你重写了这个方法,你必须调用父实现。
*/

fromViewController	
A view controller whose view is currently visible in the parent'€™s view hierarchy.
toViewController	
A child view controller whose view is not currently in the view hierarchy.

duration	
The total duration of the animations, in seconds. If you pass zero, the changes are made without animating them.

options	
A mask of options indicating how you want to perform the animations. For a list of valid constants, see UIViewAnimationOptions.

animations	
A block object containing the changes to commit to the views. Here you programmatically change any animatable properties of the views in your view hierarchy. This block takes no parameters and has no return value. This parameter must not be NULL.

completion	
A block to be called when the animation completes.
The block takes the following parameters:

finished
YES if the animation finished; NO if it was skipped.
Open in Developer Documentation

参考:
https://www.cnblogs.com/weicyNo-1/p/7299142.html
在这里插入图片描述

唐巧前辈之前的一些记录:

原来的问题

这些新增的方法和属性用于改进我们的编程方式。那么让我们先看看以前的对于 UIViewController的使用有什么潜在的问题,认清问题,我们才能理解苹果改变的意义。

在以前,一个 UIViewController 的 View 可能有很多小的子 view。这些子 view 很多时候被盖在最后,我们在最外层ViewController 的 viewDidLoad 方法中,用 addSubview 增加了大量的子 view。这些子 view大多数不会一直处于界面上,只是在某些情况下才会出现,例如登陆失败的提示 view,上传附件成功的提示 view,网络失败的提示 view等。但是虽然这些 view 很少出现,但是我们却常常一直把它们放在内存中。另外,当收到内存警告时,我们只能自己手工把这些 view 从super view 中去掉。

改变

苹果新的 API 增加了 addChildViewController 方法,并且希望我们在使用 addSubview 时,同时调用[self addChildViewController:child] 方法将 sub view 对应的 viewController也加到当前 ViewController 的管理中。对于那些当前暂时不需要显示的 subview,只通过addChildViewController 把 subViewController 加进去。需要显示时再调用 transitionFromViewController:toViewController:duration:options:animations:completion方法。
另外,当收到系统的 Memory Warning 的时候,系统也会自动把当前没有显示的 subview unload 掉,以节省内存。

参考:https://blog.devtang.com/2012/02/06/new-methods-in-uiviewcontroller-of-ios5/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值