You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 02:37:32