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

SwiftUI中含@State属性的ScrollView滚动卡顿如何解决?

SwiftUI ScrollView 状态更新导致滚动卡顿的解决方案

卡顿核心原因

只要@State的更新触发了ScrollView所在层级的body重新求值,SwiftUI就会标记整个ScrollView视图树为需要重绘。这个重绘流程和滚动手势的帧渲染跑在同一个主线程上,一旦重绘耗时稍长,就会直接打断滚动的流畅度。
之前尝试的两类优化没用,本质上是因为——不管把.onReceive修饰符挪到ScrollView内部、用什么方式更新currentTime,只要这个状态定义在最外层的ContentView上,状态变化时一定会触发整个ContentView的body重新计算,连带着ScrollView一起重绘,自然解决不了卡顿。

实际可用的优化方案

  • 缩小状态更新的影响范围,拆分独立子视图隔离容器
    把会动态变化的文本和对应的状态,一起封装成独立的子视图,完全从外层ContentView的body计算链路里剥离。这样状态更新时,只会触发这个单独子视图的重绘,根本碰不到外层的ScrollView容器,从根源上避免ScrollView因为状态变化被重绘。
    参考修正后的代码:
    // 独立持有时间状态的动态组件
    struct LiveTimeText: View {
        @State private var timeNow: Double = 0
        private let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
        
        var body: some View {
            Text("currentTime: \(timeNow)")
                .onReceive(timer) { newTime in
                    timeNow = newTime.timeIntervalSince1970
                }
        }
    }
    
    struct ContentView: View {
        var body: some View {
            ScrollView {
                VStack(spacing: 16) {
                    LiveTimeText()
                    // 正常添加需要滚动的长静态内容,完全不受时间更新影响
                    ForEach(0..<100) { index in
                        Text("长滚动列表项 \(index)")
                            .frame(maxWidth: .infinity)
                            .padding()
                    }
                }
            }
        }
    }
    
  • 必须在父层持有状态时,用EquatableView阻断多余重绘
    如果业务逻辑要求currentTime必须定义在ContentView层级,就不要把动态文本直接写在ScrollView的子视图闭包里。把动态文本抽成遵循Equatable协议的独立视图,包上EquatableView阻断视图树的连锁重绘:
    struct ContentView: View {
        @State private var currentTime: Double = 0
        private let timer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect()
        
        var body: some View {
            ScrollView {
                EquatableView(TimeShowView(time: currentTime))
                // 其余静态滚动内容
                ForEach(0..<100) { index in
                    Text("静态项 \(index)")
                }
            }
            .onReceive(timer) { newTime in
                currentTime = newTime.timeIntervalSince1970
            }
        }
    }
    
    struct TimeShowView: View, Equatable {
        let time: Double
        // 自定义等值判断,只有time值真正变化时才触发重绘
        static func == (lhs: Self, rhs: Self) -> Bool {
            lhs.time == rhs.time
        }
        
        var body: some View {
            Text("currentTime: \(time)")
        }
    }
    
  • 调整定时器的RunMode减少滚动期的渲染冲突
    滚动时主线程的run mode会切换成.tracking,如果定时器的事件刚好在滚动帧渲染的窗口期触发,就会直接和滚动渲染抢资源。可以把定时器的RunMode设置为.default,让滚动期间的事件更新排到滚动帧渲染完成后再执行,进一步减少卡顿感知。

内容的提问来源于stack exchange,提问作者joshuakcockrell

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.26 20:45:03