UIView的初始化流程

        在iOS开发中我们会经常使用UIViewaddSubView方法,将一个子视图添加到父视图中。在进行addSubview操作的时候,底层框架帮助我们将子视图加入到父视图的层级结构中,视图的层级结构其实是一种树形结构,子视图从创建到添加到父视图接着再从父视图中remove掉,会经过一系列的方法俗称为生命周期,本篇文章将围绕UIView被添加父和移除父视图做简要介绍。

  • 在UIViewController中添加子视图

  • 在UIViewController中删除子视图

在UIViewController中添加子视图

        UIViewController通常我们会称视图容器/控制器,用来加载、组织和呈现视图,UIViewController中都会有一个属性view(对于UIViewController的子类UITableViewControllerUICollectionViewControlle会有tableViewcollectionView),可以通过向view这个属性添加一些其它组件例如自定义的view、按钮来改变页面的样式以及视图的层级关系,iOS开发人员都应该知道UIVIewController的初始化流程应该是什么:

  • 加载视图(loadView、viewDidLoad)
  • 开始呈现视图(viewWillAppear
  • 对视图进行布局(viewWillLayoutSubView、viewDidLayoutSubView)
  • 呈现视图(viewDidAppear)

        但是在初始化的过程中添加子视图,不同的阶段会影响子视图初始化顺序,例如moveToSuperViewmoveToWindow的执行顺序就会因为addSubview的调用时机而不同,用以下比较简单的代码来说明:

class TestView: UIView { // subView

override func didMoveToWindow() { super.didMoveToWindow() ;print(#function) }

override func willMove(toWindow newWindow: UIWindow?) { super.willMove(toWindow: newWindow) ;print(#function) }

override func willMove(toSuperview newSuperview: UIView?) { super.willMove(toSuperview: newSuperview) ;print(#function) }

override func didMoveToSuperview() { super.didMoveToSuperview(); print(#function) }

override init(frame: CGRect) { super.init(frame: frame) ;print(#function) }

required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) ; print(#function) }

override func layoutSubviews() { super.layoutSubviews() ;print(#function) }

override func layoutSublayers(of layer: CALayer) { super.layoutSublayers(of: layer); print(#function) }

override func display(_ layer: CALayer) { print(#function) }

}


        以上代码是通过print(#function)打印view的相关方法的名字,在TestView被添加到其它视图上时,这些方法会被调用(默认实现display不会调用drawRect)。之后在ViewController的view里添加TestView作为子视图,ViewController的代码如下:

class ViewController: UIViewController {
var testView : TestView?

override func viewDidLoad() { super.viewDidLoad();print(#function) }

override func viewWillLayoutSubviews() {super.viewWillLayoutSubviews();print(#function) }

override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated);print(#function)}

override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated);print(#function) }

override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews();print(#function) }

@IBAction func remove(_ sender: Any) { self.testView?.removeFromSuperview() }

func addTestView() {
    if self.testView == nil {
        testView = TestView.init(frame: CGRect.init(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height))
        self.view.insertSubview(testView!, at: 0)
    }
}

}


        代码也很简单,其中的addTestView方法将TestView实例化并且添加到ViewControllerview上,使用insertSubview代替addSubView是因为该view上有一个按钮,点击按钮会调用remove方法,防止testView遮住按钮,将testView insert到按钮的下面,接下来分别在viewDidLoad、viewWillAppear...调用addTestView方法(注意: 我是在print方法之后添加的addTestView方法)。如下表所示:

UIView的初始化流程_第1张图片
图1

        注意观察红色框,在 viewDidLoad方法中添加 testView会先执行 moveToSuperView方法,之后会在 viewWillAppear方法之后执行 testViewwillMoveToWindow方法,也就是说,先执行 moveToSuper再执行 moveToWindow,这种顺序与在 viewWillAppear中添加 testView时是相同的。
        然而在 viewWillLayoutSubviews及之后的方法中调用 addTestView时,事情会有些变化,可以看到, willMoveToWindow先执行了,然后再执行 willMoveToSuperView,这种顺序的变化是什么原因呢?接着在 UIViewController的初始化方法中打印 window对象:
WechatIMG3.jpeg
         window对象在 viewWillLayoutSubviews()方法被调用之前都是 nil,也就是说在 windownil的情况下先将 testView移动到 superView上,在 viewWillLayoutSubviews()被调用后,再将 view移至 window上,所以在开发的时候如果在 UIViewController中调用 self.view.window.addSubView()方法时,需要判断该 window是否为空。
        还有另一点需要注意,第一幅图蓝色框部分,由于在 viewDidAppear()方法中执行 addTestView()操作,会重新执行ViewController的layout方法,而且,testView的layout方法总是在ViewController的layout方法之后执行,那么根据苹果文档上描述的 viewDidLayoutSubviews的解释可以了解到, ViewControllerviewWillLayoutSubviewsviewDidLayoutSubviews方法会在视图控制器的 viewframe、bounds.size改变的时候调用,在进行 addSubView或者 insertSubView时,如果是在 viewDidAppear()之后调用的, ViewController会重新 layout一次,接着子视图会相应的调用layout 方法,最后执行 display(drawRect、drawLayer)方法。
        那么子视图的 layoutSubviews()方法何时被调用呢?就是子视图的frame、bounds变化的时候,如果需要强制调用的话,苹果文档明确说明不建议手动调用 layoutSubviews()必要时可以调用 setNeedsLayout()layoutIfNeeded(),前者是下一次绘制周期进行 layoutSubviews(),而后者是立即执行 layoutSubviews()。如果只是父视图的 frame或者 bounds变化了,子视图是否会调用 layoutSubviews(),答案是不会调用。
        但是 TestView中的 layoutSublayers()是什么鬼?其实每一个 UIView对象都会包含一个 layerlayer才是其真正绘制到屏幕上的内容,而 UIView本身是 UIResponder的子类,用来响应 touch这类事件的(仔细想想为什么一个按钮的位置在它的父视图窗口之外就无法响应事件了),这样做的好处就是显示与响应事件分离,通常对一个view的位置以及大小的设置会选择 view.frame,但也可以通过 view.layer.frame,结果是一样的, UIViewframe是从 layerframe得到( frame是通过 layercenteranchorPointbounds计算得来),每当改变 UIView或者 UIView.layersizelayoutSubviews()layoutSublayers(:)都会被调用。

在UIViewController中删除子视图

        接下来会在ViewController不同的阶段进行testView的添加和删除,来看看在删除过程中,testView经历了哪些阶段。首先在viewDidLoad方法中添加testView并在其它几个阶段进行remove操作如下图所示:

UIView的初始化流程_第2张图片
WechatIMG4.jpeg
        在 window没有被实例化时调用 testView.removeFromSuperView()仅仅会触发 willMove(toSuperview:)didMoveToSuperview(),各执行了两次是因为每当 superView改变的时候就会调用 willMovedidMove方法,第一次 superViewViewController.view,第二次 superViewnil
        再观察从 viewWillLayoutSubviewsviewDidLayoutSubviews移除 testView,可以发现,由于此时 window已经不为空,多了 willMove(toWindow)didMoveToWindow()方法,同样在remove阶段,window为空,到目前为止,testView的display方法都没有被调用,仅仅调用了layout方法。
        最后由于调用 viewDidAppear方法时,表明此时的view已经被渲染至屏幕上,所以testView的display方法被调用,remove阶段会将 testView移出父视图和窗口,然后 ViewController会触发 layout。在其他几个阶段调用 addTestView()时也有类似的行为,可以尝试一下。

结论

  1. 在UIViewController的不同阶段添加、移除子视图时,子视图的创建过程不同。

  2. UIViewController.view.window对象是在viewWillAppear之后被初始化,如果需要在viewWillAppear或者更早的阶段使用window对象,需要调用AppDelegate的window。

  3. UIViewController.view在执行了addSubView以及子视图调用了removeFromSuperView时,都有可能调用viewWillLayoutSubviews、viewDidLayoutSubviews

  4. 当UIView的frame改变时,会调用layoutSubviews()和layoutSublayers(of:)方法,UIView的frame来自于其layer.frame。

相关链接

详解 CALayer 和 UIView 的区别和联系
UIView.frame的骗局

你可能感兴趣的:(UIView的初始化流程)