SwiftUI @Observable重绘机制探究:栈视图与自定义视图差异
SwiftUI @Observable 重绘与依赖追踪问题解析
以下是针对测试中三个问题的具体分析与解决方案:
问题1:无@Observable依赖的内联Text视图会重绘,而同逻辑的StaticText自定义视图却不会?
核心原因是视图重绘的触发范围不同:
- 内联的
Text("Why do I change color?")直接写在ContentView的body中,而ContentView的body会因clock.timeValue的变化(被SimpleSliderView依赖)而重新计算。每次body重算时,colors.randomElement()都会重新执行,导致文本颜色随机变化。 StaticText是独立的自定义View,它没有依赖任何@Observable对象的属性,SwiftUI的依赖追踪系统会判定它不需要跟随ContentView的重绘而更新,因此它的body不会被重复调用,前景色保持初始随机值不变。
问题2:外层ZStack无相关依赖却会重绘背景,但部分子视图不重绘?
- 外层ZStack属于
ContentView的body内容,当ContentView因clock.timeValue变化触发body重计算时,ZStack的背景色代码colors.randomElement()会被重新执行,导致背景色随机变化。 - 而
StaticText、StaticView这类子视图,由于自身没有绑定任何会变化的状态,SwiftUI会跳过它们的重绘流程,只更新确实依赖变化属性的视图(比如SimpleSliderView)。
问题3:如何让SwiftUI的栈视图(HStack/VStack/ZStack)避免因子视图重绘而自身重绘?
可以通过以下几种方式优化:
- 拆分独立视图:把栈中静态、不依赖变化属性的内容拆成单独的自定义View,让SwiftUI可以精准追踪每个View的依赖,避免整体重绘。比如将外层ZStack的背景和静态子视图分离。
- 使用Equatable协议:给自定义View添加
Equatable实现,并使用.equatable()修饰符,让SwiftUI可以对比View的属性变化,决定是否需要重绘。例如给StaticView实现Equatable:struct StaticView: View, Equatable { let text:String static func == (lhs: StaticView, rhs: StaticView) -> Bool { lhs.text == rhs.text } // ... 原有代码 } - 隔离状态依赖:将依赖
@Observable属性的视图单独封装,避免让父级栈视图直接接触变化的状态,减少父视图的重绘触发。 - 优化Observable对象:确保
@Observable类中只有必要的属性被追踪,用@ObservationIgnored标记不需要触发视图更新的属性,避免不必要的通知。
测试代码
import SwiftUI public func remap(input:Double, inMin:Double, inMax:Double, outMin:Double, outMax:Double) -> Double { return ((input - inMin) / (inMax - inMin) * (outMax - outMin)) + outMin; } public func remap(input:Float, inMin:Float, inMax:Float, outMin:Float, outMax:Float) -> Float { return ((input - inMin) / (inMax - inMin) * (outMax - outMin)) + outMin; } @Observable class BPMClock : Identifiable, Hashable, Equatable { var id: UUID = .init() static func == (lhs: BPMClock, rhs: BPMClock) -> Bool { return lhs.id == rhs.id } func hash(into hasher: inout Hasher) { hasher.combine(self.id) } var bpm:Double = 60.0 var timeValue:Double = 0.0 @ObservationIgnored private var rawTimeValue:Double = 0.0 @ObservationIgnored private var lastTime:Double = 0.0 @ObservationIgnored var timer:Timer? = nil func startTimer() { self.timer = Timer.scheduledTimer(timeInterval: 1/60, target: self, selector: #selector(calcTimeAbsolute), userInfo: nil, repeats: true) } @objc func calcTimeAbsolute() { let now = Date.timeIntervalSinceReferenceDate self.calcTime(usingNow: now) } func calcTime(usingNow now:Double) { let delta = now - self.lastTime self.rawTimeValue = fmod(delta + self.rawTimeValue, self.bpmToS(self.bpm) ) self.timeValue = remap(input: self.rawTimeValue, inMin: 0.0, inMax:self.bpmToS(self.bpm), outMin: 0.0, outMax: 1.0) self.lastTime = now } private func bpmToS(_ bpm:Double) -> Double { let bps = 60.0 / bpm return bps } private func sToBpm(_ s:Double) -> Double { let bpm = s * 60.0 return bpm } } struct SimpleSliderView : View { var value: Double // @Binding var value:Double private let colors = [Color.red, Color.orange, Color.yellow, Color.green, Color.blue, Color.purple] var body: some View { ZStack { colors.randomElement()! Rectangle().fill(Color.red) .frame(width: 200 * value) Text("This should obviously redraw") } .frame(width:200, height: 100 ) } } struct StaticView: View { let text:String private let colors = [Color.red, Color.orange, Color.yellow, Color.green, Color.blue, Color.purple] var body: some View { ZStack { colors.randomElement()! Text(text) } } } struct StaticText: View { private let colors = [Color.red, Color.orange, Color.yellow, Color.green, Color.blue, Color.purple] var body: some View { Text("Why does the background Redraw?") .foregroundStyle(colors.randomElement()!) } } struct ContentView: View { let clock = BPMClock() // @State var clock = BPMClock() // @Bindable var clock = BPMClock() private let colors = [Color.red, Color.orange, Color.yellow, Color.green, Color.blue, Color.purple] var body: some View { ZStack { colors.randomElement()! VStack { Text("Why do I change color?") .foregroundStyle(colors.randomElement()!) StaticText() StaticView(text: "Start Clock \n (this does not redraw)") .frame(width: 100, height: 100) .onTapGesture { self.clock.startTimer() } StaticView(text: "I should not redraw") .frame(width: 100, height: 100) SimpleSliderView(value: clock.timeValue) .frame(height: 100) } .frame(width: 400, height: 400) } } } #Preview { ContentView() }
内容的提问来源于stack exchange,提问作者vade
相关产品推荐
相关产品推荐

