SwiftUI中常量Date属性为何能被onChange检测到变化?
我在学习Kodeco的《SwiftUI Apprentice》书籍时遇到了如下代码片段:
struct CountdownView: View { let date: Date @Binding var timeRemaining: Int let size: Double var body: some View { Text("\(timeRemaining)") // 5 .font(.system(size: size, design: .rounded)) .padding() .onChange(of: date) { _ in // 6 timeRemaining -= 1 } } } struct TimerView: View { @State private var timeRemaining: Int = 3 // 1 @Binding var timerDone: Bool // 2 let size: Double var body: some View { TimelineView( // 3 .animation( minimumInterval: 1.0, paused: timeRemaining <= 0)) { context in CountdownView( // 4 date: context.date, timeRemaining: $timeRemaining, size: size) } .onChange(of: timeRemaining) { _ in if timeRemaining < 1 { timerDone = true // 7 } } } } struct TimerView_Previews: PreviewProvider { static var previews: some View { TimerView(timerDone: .constant(false), size: 90) } }
我最初以为CountdownView会被频繁重建,但那样的话date属性不会有变化。于是我给CountdownView添加了onAppear修饰符检测视图是否重建,结果发现它只被创建了一次。这让我产生疑问:尽管date是用let声明的常量且未使用@Binding,为何它的变化能被SwiftUI的onChange观察到?
解答
这得从SwiftUI的视图更新机制和TimelineView的工作逻辑说起:
视图结构体是"描述蓝图",而非实际UI实例
SwiftUI的视图结构体是值类型,你看到的"只创建一次"是指结构体的初始化仅执行一次,但这只是SwiftUI用来生成UI的蓝图。每次TimelineView触发更新时,SwiftUI会更新已有CountdownView实例的date属性值,而非重新创建新的结构体实例——视图实例的生命周期和属性更新由SwiftUI框架托管,不是开发者直接控制的。onChange观察的是值的变化,和属性修饰无关
onChange(of:)的核心逻辑是对比前后两次传入的值是否不同,不管这个值来自let常量还是@Binding变量。在TimelineView的动画模式下,每隔1秒就会生成新的context.date,SwiftUI会把这个新值传递给CountdownView的date属性,只要前后值不一样,onChange就会触发。let是代码层面的约束,不限制SwiftUI内部操作
代码里用let声明date,是约束开发者自己的代码不能修改它,但SwiftUI框架不受这个限制。SwiftUI在管理视图树时,会根据状态变化更新视图实例的属性值,哪怕是let声明的属性——因为你写的视图结构体只是描述UI应该是什么样,实际的UI实例由SwiftUI全权管理。
简单总结:你写的视图结构体是给SwiftUI看的"说明书",SwiftUI会根据说明书创建并维护实际的UI对象,它可以更新对象的属性值;而onChange只关心传入值的前后差异,和属性是不是let、有没有绑定无关。
内容的提问来源于Stack Exchange,提问作者ceylanburak

