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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 05:17:34