使用 @ViewLoading 优化视图代码
今天是 WWDC 23 的第三天,依旧是在疯狂刷讲座视频。今天在一个讲座中提到了一个 API 引起了我的注意,算是解决了我们在编写视图代码时的一个痛点,尤其是对强迫症而言非常友好。
编写视图代码时的问题
过去我们在写 UIViewControlller
的代码时,总是会把一些 View 作为 UIViewControlller
的属性定义在头部。此时我们就遇到了一个问题。对于部分简单的 View 可以直接在定义属性的时候初始化,还可以把属性标记成 let 。强迫症表示非常满意。
但有的时候,View 的初始化依赖于其他配置或者参数,没办法直接在定义时完成初始化。遇到这种情况,我们一般有两种方法解决:
- 使用
lazy
关键字延迟初始化,此时上下文可以拿到所有参数。(如dateLabel3
) - 使用 Optional 标记参数并赋初始值
nil
,之后再初始化。(如dateLabel2
)
class DateViewController: UIViewController {
private let dateLabel1 = UILabel()
private var dateLabel2: UILabel? = nil
private var lazy dateLabel3: UILabel = {
return UILabel()
}()
override func viewDidLoad() {
super.viewDidLoad()
self.dateLabel2 = UILabel(frame: self.view.bounds)
}
}
实际上,使用 lazy
关键字是很多人的选择。甚至很多人会将所有 View 的初始化工作都用 lazy
来实现。一来可以将所有 View 相关的初始化和配置代码都放在一起,二来所有 View 也都支持了按需加载,对于部分后出现的 View 会有性能优势。
我个人对
lazy
不是那么喜欢,因为一方面它会导致大量逻辑代码堆积在文件头部,不利于查看。另一方面绝大部分 View 其实都是随着 ViewController 同步加载的,懒加载的意义不大,而且对于后续不会更改的 View 使用 var 关键字也很奇怪。
使用 Optional 的问题是之后所有用到该属性的时候都需要判空,这样的代码非常不优雅。
@ViewLoading
不管怎么样,Apple 推出了一个新的属性包装器 @ViewLoading
,可以帮助我们解决上面提到的第二种情况。当使用 @ViewLoading
标记参数时,参数可以被定义成 var 同时没有初始值,此时编译器不会报错,而是认为该参数在此时没有值但在稍后调用时一定会有值(类似于在类型后加了强解包!)。当调用这个参数时,如果系统发现这个值此时是 nil,会先现调用 viewDidLoad ,然后再调用这个参数。这样就能保证这个参数在调用的时候一定有值。
理解上面这个原理的人应该发现了,要真正满足这个条件的话,一定要在 viewDidLoad 中初始化对应参数,因为系统默认被标记的属性在 viewDidLoad 中完成了初始化。如果没有这么做,执行到调用参数的地方时还是会报错。
class DateViewController: UIViewController {
@ViewLoading private var dateLabel: UILabel
var date: Date? {
didSet {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .full
let dateString = dateFormatter.string(from: self.date ?? Date())
// 如果执行下面这段代码时发现 dateLabel 为 nil,
// 会先执行 viewDidLoad ,然后再执行下面的代码
self.dateLabel.text = dateString
}
}
override func viewDidLoad() {
super.viewDidLoad()
let label = UILabel(frame: self.view.bounds)
self.view.addSubview(label)
self.dateLabel = label
}
}
那么什么条件下会出现 viewDidLoad 没有走但直接调用参数的情况呢?官方给了一个例子。先初始化了一个 ViewController ,在没有 push 或 present 的情况下间接调用了 dateLabel
。
let dateViewController = DateViewController()
dateViewController.date = Date()
@ViewLoading
不仅能够帮助我们优化视图编码,对于 StoryBoard 的参数也适用(甚至我觉得这个新特性就是为了 IBOutlet
而生的)。以往我们从 StoryBoard 或 XIB 拖出来的参数都是强解包的。这是说的通的,因为这些参数在定义时还没有被初始化,但在使用这些参数时(viewDidLoad
)必然已经初始化好了。使用强解包也是为了方便我们调用,否则所有使用到参数的地方都得判断一下是否为空。但强解包是苹果非常不建议的行为,过去这种写法是打苹果的脸。如今 @ViewLoading
出现可以更加优雅的解决这个代码层面的问题。
// old
@IBOutlet private var label: UILabel!
// now
@IBOutlet @ViewLoading private var dateLabel: UILabel
最后提供一个好消息和一个坏消息:
- 好消息是这个 API 同时提供了 UIKit 版 和 APPKit 版,macOS 也能用上。
- 坏消息是 iOS 16.4+ ,几乎是不可用的状态。。。
总结
如果熟悉 Android 的同学应该知道,@ViewLoading
和 Kotlin 的 LateInit
非常相似,LateInit
其实也是为了优化 Android XML 参数在代码层面的表示问题。而且 LateInit
的语义更加易懂,表明修饰的参数现在没有被初始化,但保证会在稍后初始化。不同的是 @ViewLoading
把使用场景限定在了视图相关的内容上,并且增加了在调用参数前判断是否为空,为空的话自动调用 viewDidLoad 的逻辑。而 Kotlin 的 LateInit
则更加通用。
相比起
@ViewLoading
, 我其实更想要lazy let
。