初探 iOS 中自定义 UIView 的初始化过程

4,077

awakeFromNib()
init(frame:)
init(coder:)

  • Info:
    • macOS 10.12.1
    • Xcode 8.2 Beta 1
    • Swift 3.0

前言

在 StoryBoard 和 Xib 出现之后,iOS UI 开发出现了三足鼎立之势。本文不涉及 StoryBoard、Xib、纯代码的优劣之分。仅仅涉及几个初始化方法:awakeFromNib() & init(frame:) & init(coder:),探讨他们何时调用,为何调用。

Xib & Nib

ib 是 Interface Builder 的缩写,即界面构造器。这里简要说下,Xib 和 Nib 各是什么,有什么区别。

Xib 实际是一个 XML 文件,而 Nib 是二进制文件。当应用编译时,Xib 文件被翻译为 Nib。所以在 Xcode 中,我们可以自己新建 Xib 文件来构造 UI,而当编译时,Xcode 会自动生成相应的 Nib 文件,而不需我们额外关注。关于其详细介绍,您可以参考文末的资料。

OK! Talk is cheap, show me the code!

Demo

在下面的 Demo 中,统一将自定义 UIView 命名为 MyView。

MyView.swift

import UIKit

class MyView: UIView {
    // methods
}

Interface Builder

如果使用 Interface Builder 拖控件,那么其默认属于 UIView 类型。为将其改为自定义控件,需要将 Utilities 中 Identity inspector 的 Custom Class 改为 MyView。


Custom Class 改为 MyView

为了方便看出调用顺序,将 MyView.swift 改为如下:

import UIKit

class MyView: UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)

        print("init(frame:)")
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        print("init(coder:)")
        // fatalError("init(coder:) has not been implemented")
    }

    override func awakeFromNib() {
        super.awakeFromNib()

        print("awakeFromNib()")
    }
}

之后运行即可在屏幕上看到该自定义 UIView,控制台输出:

init(coder:)
awakeFromNib()

小结

通过打印的输出,可以看出使用 Interface Builder 载入 View 不会调用 init(frame:) 方法,而是调用了 init(coder:)init(coder:) 是 NSCoding 协议中的方法,NSCoding 是负责编码解码,归档处理的协议。

required init?(coder aDecoder: NSCoder)

代码中的 init(coder:) 与平时见到的其他初始化方法有点不同:required 是指其为必要构造器,即子类「必须」重写该构造器,但当父类的构造器可以完全满足初始化时,也可不重写。init? 是指其为可失败构造器,即其可以 return nil 告知外界构造失败。若想详细了解 Swift 中的构造器,可以参考苹果官方文档。

init(coder:) 的调用处于 Nib 载入时,而 awakeFromNib() 的调用处于 Nib 载入后。Nib 的载入过程如下:

  1. Nib 文件内容和引用的资源文件加载到内存;
  2. 反归档存储于 Nib 文件的图像数据对象并初始化;
    1. 遵从 NSCoding 的对象(UIView & UIViewController)调用 init(coder:)
    2. 其他对象调用其他构造器方法
  3. 建立对象间连接:Outlet & Action
  4. 实现 awakeFromNib() 的对象调用该方法

需要注意的是,awakeFromNib() 中需要调用父类的该方法以保证父类的进行额外初始化。而在本例中重写的 init(coder:) 目的主要是查看调用顺序,并没有加入特别的操作。因此在实际使用中,如果使用 Interface Builder,可以不重写该方法。

Code

MyView.swift

import UIKit

class MyView: UIView {

    override init(frame: CGRect) {
        super.init(frame: frame)

        print("init(frame:)")
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func awakeFromNib() {
        super.awakeFromNib()

        print("awakeFromNib")
    }
}

ViewController.swift

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let myView = MyView(frame: CGRect(x: 100, y: 100, width: 50, height: 50))
        myView.backgroundColor =  .black
        view.addSubview(myView)
    }

}

之后运行即可在屏幕上看到该自定义 UIView,控制台输出:

init(frame:)

小结

通过纯代码创建自定义 UIView,便只调用 init(frame:) 方法,不涉及 Nib 的方法,因此不会调用 awakeFromNib()init(coder:) 方法。而由于 init(coder:) 为必要构造器,因此重写 init(frame:) 时,必须实现该方法。

有时,为了便于从 Interface Builder 和纯代码都能创建自定义 UIView 对象,可以将 init(coder:) 方法改为:

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    // fatalError("init(coder:) has not been implemented")
}

若保留 fatalError(),则从 Nib 初始化时会无条件输出语句并停止运行。

后记

可能是强迫症作祟,学习中每遇到一个知识点,都想要查看官方文档或者 Google 出为什么,然后自己敲代码验证,再总结出一篇文章,投稿给简书、掘金。一篇文章有时要耗费一两天,因为查阅的资料都是略有过时且几乎全为英文,但自己挺享受这样的学习状态,也很享受分享给大家之后获得的收藏所带来的鼓励。最近也看了很多实习生的招聘,现在深感基础的重要,未来可能会倾向一些基础,例如数据结构、算法、网络等知识。也希望自己在寒假或下学期能找一份 iOS 实习,虽然自己也有所涉猎 Android 等其他的一些技术栈,但还是对 iOS 最感兴趣。Come on!

参考资料

Nib Files
Initialization