今天是 WWDC 23 的第三天,依旧是在疯狂刷讲座视频。今天在一个讲座中说到了一个 API 引起了我的留意,算是处理了咱们在编写视图代码时的一个痛点,尤其是对强迫症而言非常友好。

编写视图代码时的问题

曩昔咱们在写 UIViewControlller 的代码时,总是会把一些 View 作为 UIViewControlller 的特点界说在头部。此刻咱们就遇到了一个问题。关于部分简单的 View 可以直接在界说特点的时分初始化,还可以把特点符号成 let 。强迫症标明非常满意。

但有的时分,View 的初始化依赖于其他配置或者参数,没办法直接在界说时完结初始化。遇到这种状况,咱们一般有两种办法处理:

  1. 运用 lazy 关键字推迟初始化,此刻上下文可以拿到一切参数。(如 dateLabel3
  2. 运用 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