在iOS开发中我们会经常使用UIView
的addSubView
方法,将一个子视图添加到父视图中。在进行addSubview
操作的时候,底层框架帮助我们将子视图加入到父视图的层级结构中,视图的层级结构其实是一种树形结构,子视图从创建到添加到父视图接着再从父视图中remove
掉,会经过一系列的方法俗称为生命周期,本篇文章将围绕UIView
被添加父和移除父视图做简要介绍。
在UIViewController中添加子视图
在UIViewController中删除子视图
在UIViewController中添加子视图
UIViewController
通常我们会称视图容器/控制器,用来加载、组织和呈现视图,UIViewController
中都会有一个属性view(对于UIViewController
的子类UITableViewController
和UICollectionViewControlle
会有tableView
和collectionView
),可以通过向view这个属性添加一些其它组件例如自定义的view、按钮来改变页面的样式以及视图的层级关系,iOS开发人员都应该知道UIVIewController
的初始化流程应该是什么:
- 加载视图(
loadView、viewDidLoad
)
- 开始呈现视图(
viewWillAppear
) - 对视图进行布局(
viewWillLayoutSubView、viewDidLayoutSubView
) - 呈现视图(
viewDidAppear
)
但是在初始化的过程中添加子视图,不同的阶段会影响子视图初始化顺序,例如moveToSuperView
和moveToWindow
的执行顺序就会因为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
实例化并且添加到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()
时也有类似的行为,可以尝试一下。
结论
- 在UIViewController的不同阶段添加、移除子视图时,子视图的创建过程不同。
- UIViewController.view.window对象是在viewWillAppear之后被初始化,如果需要在viewWillAppear或者更早的阶段使用window对象,需要调用AppDelegate的window。
- UIViewController.view在执行了addSubView以及子视图调用了removeFromSuperView时,都有可能调用viewWillLayoutSubviews、viewDidLayoutSubviews
- 当UIView的frame改变时,会调用layoutSubviews()和layoutSublayers(of:)方法,UIView的frame来自于其layer.frame。