SwiftUI滚动时用Combine Throttle,视图停止更新如何解决?
问题
我把应用改成用Combine的throttle操作符减缓屏幕更新,整体运行正常,但滚动时视图会停止更新。点击「生成示例数据」后滚动,视图得等滚动停止才会更新。目前throttle已经用了RunLoop.Main,能不能在使用throttle的同时保持滚动中的视图更新?
我在iOS 17上使用Observable框架,所以用@State而非@StateObject,代码和Combine throttle的教程示例几乎一致。
代码示例:
import SwiftUI import Combine import Observation struct Data { var a = 0.0 var b = 0.0 var c = 0.0 } /// 数据中心存储 public final class Store: @unchecked Sendable { private(set) var data = PassthroughSubject<[Data], Never>() // 单例实例 public static let sharedStore = Store() var timer: Timer? = nil public func generateSampleData() { timer = Timer(timeInterval: 0.025, repeats: true) { [weak self] _ in self?.data.send([Data(a: Double.random(in: (-32000)..<(32000)))]) } RunLoop.current.add(timer!, forMode: .common) } public func stopSampleData() { timer?.invalidate() timer = nil } } @Observable public final class ViewModel { private var cancellables = Set<AnyCancellable>() private(set) var dataSet = [Data()] init() { Store.sharedStore.data .throttle(for: .milliseconds(100), scheduler: RunLoop.main, latest: true) .sink { [weak self] values in self?.dataSet = values } .store(in: &cancellables) } } @main struct IBTTestApp: App { @State var generateSamples = false @State var viewModel = ViewModel() var body: some Scene { WindowGroup { Group { List { LabeledContent("Generate sample data") { Toggle("", isOn: $generateSamples) .onChange(of: generateSamples) { generateSamples ? Store.sharedStore.generateSampleData() : Store.sharedStore.stopSampleData() } } Text("\(viewModel.dataSet[0].a)") } } } } }
解决方案
问题根源是RunLoop.main的默认运行模式:滚动时UIKit会切换到.tracking模式,而默认的.default模式任务会被暂停,导致throttle的事件无法及时分发到视图。
有两种简单的解决方法:
方法一:改用DispatchQueue.main作为调度器
直接替换throttle的scheduler参数为DispatchQueue.main,主队列不受RunLoop运行模式影响,滚动时依然能正常更新视图:
// ViewModel的init方法中修改 Store.sharedStore.data .throttle(for: .milliseconds(100), scheduler: DispatchQueue.main, latest: true) .sink { [weak self] values in self?.dataSet = values } .store(in: &cancellables)
方法二:让RunLoop在.common模式下工作
如果必须用RunLoop,可以自定义一个使用.common模式的调度器(.common包含了.default和.tracking等模式):
// 扩展RunLoop,添加common模式的调度器 extension RunLoop { static var mainCommon: RunLoop { let runLoop = RunLoop.main runLoop.perform(inModes: [.common]) { } return runLoop } } // 在ViewModel中使用 Store.sharedStore.data .throttle(for: .milliseconds(100), scheduler: RunLoop.mainCommon, latest: true) .sink { [weak self] values in self?.dataSet = values } .store(in: &cancellables)
另外,你的Store中Timer已经添加到.common模式下,这部分是正确的,不需要修改。
内容的提问来源于stack exchange,提问作者Phantom59
相关产品推荐
相关产品推荐

