UIView的初始化流程

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

  • <h3>在UIViewController中添加子视图</h3>
  • <h3>在UIViewController中删除子视图</h3>

在UIViewController中添加子视图

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

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

        但是在初始化的过程中添加子视图,<b>不同的阶段会影响子视图初始化顺序</b>,例如moveToSuperViewmoveToWindow的执行顺序就会因为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实例化并且添加到ViewControllerview上,使用insertSubview代替addSubView是因为该view上有一个按钮,点击按钮会调用remove方法,防止testView遮住按钮,将testView insert到按钮的下面,接下来分别在viewDidLoad、viewWillAppear...调用addTestView方法(注意: 我是在print方法之后添加的addTestView方法)。如下表所示:

图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操作如下图所示:

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()时也有类似的行为,可以尝试一下。

结论

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容