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
相关产品推荐
相关产品推荐

