在iOS开发中我们会经常使用UIView
的addSubView
方法,将一个子视图添加到父视图中。在进行addSubview
操作的时候,底层框架帮助我们将子视图加入到父视图的层级结构中,视图的层级结构其实是一种树形结构,子视图从创建到添加到父视图接着再从父视图中remove
掉,会经过一系列的方法俗称为生命周期,本篇文章将围绕UIView
被添加父和移除父视图做简要介绍。
- <h3>在UIViewController中添加子视图</h3>
- <h3>在UIViewController中删除子视图</h3>
在UIViewController中添加子视图
UIViewController
通常我们会称视图容器/控制器,用来加载、组织和呈现视图,UIViewController
中都会有一个属性view(对于UIViewController
的子类UITableViewController
和UICollectionViewControlle
会有tableView
和collectionView
),可以通过向view这个属性添加一些其它组件例如自定义的view、按钮来改变页面的样式以及视图的层级关系,iOS开发人员都应该知道UIVIewController
的初始化流程应该是什么:
- 加载视图(
loadView、viewDidLoad
)
- 开始呈现视图(
viewWillAppear
) - 对视图进行布局(
viewWillLayoutSubView、viewDidLayoutSubView
) - 呈现视图(
viewDidAppear
)
但是在初始化的过程中添加子视图,<b>不同的阶段会影响子视图初始化顺序</b>,例如moveToSuperView
和moveToWindow
的执行顺序就会因为addSubview
的调用时机而不同,用以下比较简单的代码来说明:
<pre><code>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) }
}
</code></pre>
以上代码是通过print(#function)
打印view的相关方法的名字,在TestView
被添加到其它视图上时,这些方法会被调用(默认实现display
不会调用drawRect
)。之后在ViewController
的view里添加TestView
作为子视图,ViewController
的代码如下:
<pre><code>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)
}
}
}
</code></pre>
代码也很简单,其中的addTestView
方法将TestView
实例化并且添加到ViewController
的view
上,使用insertSubview
代替addSubView
是因为该view
上有一个按钮,点击按钮会调用remove
方法,防止testView
遮住按钮,将testView insert到按钮的下面,接下来分别在viewDidLoad、viewWillAppear...
调用addTestView
方法(注意: 我是在print
方法之后添加的addTestView
方法)。如下表所示:
注意观察红色框,在
viewDidLoad
方法中添加testView
会先执行moveToSuperView
方法,之后会在viewWillAppear
方法之后执行testView
的willMoveToWindow
方法,也就是说,先执行moveToSuper
再执行moveToWindow
,这种顺序与在viewWillAppear
中添加testView
时是相同的。然而在
viewWillLayoutSubviews
及之后的方法中调用addTestView
时,事情会有些变化,可以看到,willMoveToWindow
先执行了,然后再执行willMoveToSuperView
,这种顺序的变化是什么原因呢?接着在UIViewController
的初始化方法中打印window
对象:window
对象在viewWillLayoutSubviews()
方法被调用之前都是nil
,也就是说在window
为nil
的情况下先将testView
移动到superView
上,在viewWillLayoutSubviews()
被调用后,再将view
移至window
上,所以在开发的时候如果在UIViewController
中调用self.view.window.addSubView()
方法时,需要判断该window
是否为空。还有另一点需要注意,第一幅图蓝色框部分,由于在
viewDidAppear()
方法中执行addTestView()
操作,会重新执行ViewController的layout方法,而且,testView的layout方法总是在ViewController的layout方法之后执行,那么根据苹果文档上描述的viewDidLayoutSubviews
的解释可以了解到,ViewController
的viewWillLayoutSubviews
、viewDidLayoutSubviews
方法会在视图控制器的view
的frame、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
对象都会包含一个layer
,layer
才是其真正绘制到屏幕上的内容,而UIView
本身是UIResponder
的子类,用来响应touch
这类事件的(仔细想想为什么一个按钮的位置在它的父视图窗口之外就无法响应事件了),这样做的好处就是显示与响应事件分离,通常对一个view的位置以及大小的设置会选择view.frame
,但也可以通过view.layer.frame
,结果是一样的,UIView
的frame
是从layer
的frame
得到(frame
是通过layer
的center
、anchorPoint
、bounds
计算得来),每当改变UIView
或者UIView.layer
的size
,layoutSubviews()
、layoutSublayers(:)
都会被调用。
在UIViewController中删除子视图
接下来会在ViewController
不同的阶段进行testView
的添加和删除,来看看在删除过程中,testView经历了哪些阶段。首先在viewDidLoad
方法中添加testView
并在其它几个阶段进行remove
操作如下图所示:
window
没有被实例化时调用testView.removeFromSuperView()
仅仅会触发willMove(toSuperview:)
、didMoveToSuperview()
,各执行了两次是因为每当superView
改变的时候就会调用willMove
和didMove
方法,第一次superView
为ViewController.view
,第二次superView
为nil
。再观察从
viewWillLayoutSubviews
和viewDidLayoutSubviews
移除testView
,可以发现,由于此时window
已经不为空,多了willMove(toWindow)
和didMoveToWindow()
方法,同样在remove阶段,window为空,到目前为止,testView的display方法都没有被调用,仅仅调用了layout方法。最后由于调用
viewDidAppear
方法时,表明此时的view已经被渲染至屏幕上,所以testView的display方法被调用,remove阶段会将testView
移出父视图和窗口,然后ViewController
会触发layout
。在其他几个阶段调用addTestView()
时也有类似的行为,可以尝试一下。
结论
<ol><li>在UIViewController的不同阶段添加、移除子视图时,子视图的创建过程不同。</li>
<li>UIViewController.view.window对象是在viewWillAppear之后被初始化,如果需要在viewWillAppear或者更早的阶段使用window对象,需要调用AppDelegate的window。</li>
<li>UIViewController.view在执行了addSubView以及子视图调用了removeFromSuperView时,都<b>有可能</b>调用viewWillLayoutSubviews、viewDidLayoutSubviews</li>
<li>当UIView的frame改变时,会调用layoutSubviews()和layoutSublayers(of:)方法,UIView的frame来自于其layer.frame。</li></ol>
相关链接
<a href="http://www.cocoachina.com/ios/20150828/13244.html">详解 CALayer 和 UIView 的区别和联系</a>
<a href="http://www.jcodecraeer.com/IOS/2015/0204/2413.html">UIView.frame的骗局</a>