使用 @ViewLoading 优化视图代码

今天是 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

全部评论

相关推荐

11-08 13:58
门头沟学院 Java
程序员小白条:竟然是蓝桥杯人才doge,还要花钱申领的offer,这么好的公司哪里去找
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务