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

SwiftUI多倒计时计时器异常:出现秒数延迟问题求助

问题分析与解决

核心问题

  1. 多Timer实例抢占主线程:每个LotCard都创建独立的Timer.publish,大量Timer同时运行会导致主线程调度压力,引发倒计时延迟。
  2. 本地倒计时与服务器时间不同步:依赖本地Timer递减秒数,当SignalR推送新的结束时间时,无法快速对齐;且Timer取消逻辑有误(autoconnect的Timer取消后无法自动重启)。
  3. 状态管理混乱:@State的timeRemaining属于视图本地状态,与ViewModel状态分离,易出现数据不一致。

修复方案

1. 全局单例Timer替代多实例

不要给每个LotCard单独创建Timer,用一个全局Timer驱动所有倒计时更新,降低主线程开销:

// 全局Timer单例
class TimerManager: ObservableObject {
    static let shared = TimerManager()
    let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
}

2. 基于服务器结束时间计算剩余时长

把倒计时逻辑移到ViewModel,直接用服务器返回的结束时间戳计算剩余秒数,避免本地递减的误差:

// LotCardViewModel示例
class LotCardViewModel: ObservableObject {
    @Published var showTimer = true
    @Published var endTimestamp: TimeInterval // 服务器返回的结束时间戳(单位:秒)
    
    var timeRemaining: Int {
        let currentTime = Date().timeIntervalSince1970
        let remaining = Int(endTimestamp - currentTime)
        return max(0, remaining)
    }
}

3. 重构LotCard视图

移除本地Timer,订阅全局Timer,直接使用ViewModel计算的剩余时长,同时兼容SignalR更新:

struct LotCard: View {
    @ObservedObject var lotCardViewModel: LotCardViewModel
    let lotDto: LotDto
    let fnStartLotConnection: (String) -> Void
    
    // 订阅全局Timer
    @StateObject private var timerManager = TimerManager.shared
    
    var body: some View {
        HStack(spacing: 3){
            Image(systemName: "stopwatch")
            if lotCardViewModel.showTimer {
                Text("\(lotCardViewModel.timeRemaining.toTimeString())")
                    .font(.subheadline)
                    .onReceive(timerManager.timer) { _ in
                        if lotCardViewModel.timeRemaining <= 0 {
                            lotCardViewModel.showTimer = false
                        }
                    }
            }
        }
        .onAppear {
            fnStartLotConnection(lotDto.uID)
        }
        // SignalR推送新的endTimestamp时,ViewModel的@Published会自动触发视图更新
    }
}

4. SignalR更新处理

当SignalR接收到新的结束时间时,直接更新ViewModel的endTimestamp即可:

// 在lotService的SignalR回调中
func handleLotUpdate(lotId: String, newEndTimestamp: TimeInterval) {
    if let index = getLotListDto.firstIndex(where: { $0.uID == lotId }) {
        getLotListDto[index].lotCardViewModel.endTimestamp = newEndTimestamp
        getLotListDto[index].lotCardViewModel.showTimer = true // 若之前已结束,重新显示计时器
    }
}

额外优化点

  • 避免在ForEach中使用$item传递绑定,改用@ObservedObject管理ViewModel,减少不必要的视图刷新。
  • 给toTimeString()方法添加边界处理,当剩余时间为0时显示"已结束"等提示文本。
  • 全局Timer开销极低,无需在倒计时结束后取消订阅,通过showTimer控制视图显示即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.25 04:54:16