计时器 倒计时模式下 如果 停止时检查本次运行积累总时间是否大于目标时间 如果大于目标时间 就讲...

생성일: 2025년 5월 9일

답변에 사용된 모델 O3 by Chat01

질문

计时器

倒计时模式下
如果 停止时检查本次运行积累总时间是否大于目标时间 如果大于目标时间
就讲本段落暂停时间段进行删减
确保 如果大于目标时间 本次记录的总时间是目标时间

import Foundation

enum OfflineTimerMode: String, Codable {
case positive
case countdown
}

struct OfflineTimerStatus: Codable, CustomStringConvertible {
let type: OfflineTimerType
let state: OfflineTimerState // 当前状态

let startTime: Date // 开始时点
let endTime: Date? // 停止时点

let runingStartTime: Date? // 从哪个时点开始的,只有当 type == running 时才有意义
let accumulatedSec: Int64 // 累计的秒数,不包含正在进行的

let pauseCount: Int

let timerMode: OfflineTimerMode
let targetDurationSec: Int64?

let stopReason: StopReason?

var description: String {
return
"(type)-(state) mode:(timerMode) target:(targetDurationSec ?? -1) ((startTime.secStr())-(endTime?.secStr() ?? "?")) running((runingStartTime?.secStr() ?? "?")) accumulatedSec (accumulatedSec) pauseCount (pauseCount) stopReason: (stopReason?.rawValue ?? "nil")"
}

init(
type: OfflineTimerType, state: OfflineTimerState, startTime: Date, endTime: Date?,
runingStartTime: Date?, accumulatedSec: Int64, pauseCount: Int, timerMode: OfflineTimerMode,
targetDurationSec: Int64?, stopReason: StopReason? = nil
) {
self.type = type
self.state = state
self.startTime = startTime
self.endTime = endTime
self.runingStartTime = runingStartTime
self.accumulatedSec = accumulatedSec
self.pauseCount = pauseCount
self.timerMode = timerMode
self.targetDurationSec = (timerMode == .countdown) ? targetDurationSec : nil
self.stopReason = (state == .stoped) ? stopReason : nil
if let target = self.targetDurationSec, target <= 0 {
print("Warning: targetDurationSec should be positive for countdown mode.")
}
}

func getOfflineDurSec() -> Int64 {
if state == .running, let runingStartTime = runingStartTime {
return accumulatedSec + Int64(Date.now.timeIntervalSince(runingStartTime))
}
return accumulatedSec
}

var remainingSec: Int64? {
guard timerMode == .countdown, let target = targetDurationSec else { return nil }
let elapsed = getOfflineDurSec()
return max(0, target - elapsed)
}
}

enum OfflineTimerState: String, Codable {
case running
case paused
case stoped
}

enum StopReason: String, Codable {
case manual = "manual"
case completed = "completed"
}

//
// OfflineTimerHelper.swift
// TimeMiner
//
// Created by 张磊 on 2024/4/25.
//

import ActivityKit
import DeviceActivity
import Foundation

/// 离线计时器事件的委托协议
protocol OfflineTimerDelegate: AnyObject {
/// 计时器停止时调用
func offlineTimerDidStop(track: TimeTrack)

/// 计时器暂停时调用
func offlineTimerDidPause(track: TimeTrack)
}

class OfflineTimerHelper {

static weak var delegate: OfflineTimerDelegate?

@available(iOS 17.0, *)
private static var liveActivityManager: LiveActivityManager? {
if !StoreHelper.getBool(key: StoreKeys.can_use_pro_features) {
Task {
await LiveActivityManager.shared.endActivity()
}
return nil
}
return LiveActivityManager.shared
}

static let emoji = "⌚️"

// > 更新 offline timer
// > 停掉 limit/recharge 的 monitor
// > shield掉所有app
static func start(type: OfflineTimerType, mode: OfflineTimerMode, durationSec: Int64?) {
if mode == .countdown && (durationSec ?? 0) <= 0 {
print("Error: Countdown duration must be positive.")
return
}

text
FileLog.log( msg: "\(emoji) timer start \(type.emoji) mode: \(mode) duration: \(durationSec ?? -1)") let status = OfflineTimerStatus( type: type, state: .running, startTime: .now, endTime: nil, runingStartTime: .now, accumulatedSec: 0, pauseCount: 0, timerMode: mode, targetDurationSec: durationSec ) OfflineTimerStoreHelper.saveStatus(status) if #available(iOS 17.0, *) { Task { if mode == .countdown, let duration = durationSec { _ = await liveActivityManager?.startActivity( timerType: type, timerMode: .countdown, targetDuration: TimeInterval(duration) ) } else { _ = await liveActivityManager?.startActivity(timerType: type) } } } stopOtherMonitorsShieldAllAndStartTimer(type: type)

}

static func getOfflineStatusShield() -> ShieldStatus? {
if let t = OfflineTimerStoreHelper.getStatus()?.type {
return ShieldStatus(type: .offline, offlineType: t)
}
return nil
}

private static func stopOtherMonitorsShieldAllAndStartTimer(type: OfflineTimerType) {
ShieldHelper.refreshOnMain()
}

// > 更新 offline timer
// > 刷新 shield settings
static func pause() {
FileLog.log(msg: "(emoji) timer pause")
var accumulatedTime: TimeInterval = 0
var pausedTrack: TimeTrack? = nil

text
OfflineTimerStoreHelper.updateStatus(changing: { old in if let status = old, let runingStartTime = status.runingStartTime { let now = Date.now let timeSinceRunningStart = now.timeIntervalSince(runingStartTime) let totalSec = max(Int64(0), Int64(timeSinceRunningStart)) + status.accumulatedSec accumulatedTime = TimeInterval(totalSec) let track = TimeTrack( start: runingStartTime, end: now, type: .offlineTimer, offlineType: status.type) pausedTrack = track // 判断时间段是否大于60秒 let timeSegmentIsSignificant = timeSinceRunningStart > 60 if timeSegmentIsSignificant { // 非主进程时,直接保存到TimeTrackStore if !AppInfo.isMainProcess() { TimeTrackStore.save(track: track).wait() FileLog.log( msg: "Pause: Track saved to TimeTrackStore (time segment: \(timeSinceRunningStart)s)") } else if AppInfo.isMainProcess() { // 主进程时,通过委托通知 DispatchQueue.main.async { delegate?.offlineTimerDidPause(track: track) FileLog.log(msg: "通过委托通知计时器暂停 (时间段 > 60s): \(track.description)") } } } return OfflineTimerStatus( type: status.type, state: .paused, startTime: status.startTime, endTime: nil, runingStartTime: nil, accumulatedSec: totalSec, pauseCount: status.pauseCount + 1, timerMode: status.timerMode, targetDurationSec: status.targetDurationSec ) } return nil }) if #available(iOS 17.0, *) { Task { await liveActivityManager?.updateActivityForPause(accumulatedTime: accumulatedTime) } } OfflineTimerNotification.cancelPauseNoti() onTimerPauseOrStop()

}

private static func onTimerPauseOrStop() {
ShieldHelper.refreshOnMain()
}

// > 更新 offline timer
// > 停掉 limit/recharge 的 monitor
// > shield掉所有app
static func resume() {
if EarlySleepHelp.isRunning() {
return
}
FileLog.log(msg: "(emoji) timer resume")
var type: OfflineTimerType?
var accumulatedTime: TimeInterval = 0

text
OfflineTimerStoreHelper.updateStatus(changing: { old in if let status = old { type = status.type accumulatedTime = TimeInterval(status.accumulatedSec) return OfflineTimerStatus( type: status.type, state: .running, startTime: status.startTime, endTime: nil, runingStartTime: .now, accumulatedSec: status.accumulatedSec, pauseCount: status.pauseCount, timerMode: status.timerMode, targetDurationSec: status.targetDurationSec ) } return nil }) if #available(iOS 17.0, *) { Task { await liveActivityManager?.updateActivityForResume(accumulatedTime: accumulatedTime) } } if let t = type { stopOtherMonitorsShieldAllAndStartTimer(type: t) }

}

// > 更新 offline timer 状态为 stoped,等待用户下次打开app时存到db
// > 启动 limit/recharge monitor
// > 刷新 shield settings
static func stop(reason: StopReason = .manual) {
var finalAccumulatedSec: Int64 = 0
var finalEndTime: Date? = nil
var stoppedTrack: TimeTrack? = nil

text
if let status = OfflineTimerStoreHelper.getStatus(), status.state == .running, let runningStart = status.runingStartTime { let now = Date.now let timeSinceRunningStart = now.timeIntervalSince(runningStart) let currentRunningTime = Int64(timeSinceRunningStart) let totalTime = status.accumulatedSec + currentRunningTime let track = TimeTrack( start: runningStart, end: now, type: .offlineTimer, offlineType: status.type) stoppedTrack = track // 判断时间段是否有意义 let timeSegmentIsSignificant = timeSinceRunningStart > 60 if timeSegmentIsSignificant { // 非主进程时,直接保存到TimeTrackStore if !AppInfo.isMainProcess() { TimeTrackStore.save(track: track).wait() FileLog.log( msg: "Stop: Track saved to TimeTrackStore (time segment: \(timeSinceRunningStart)s)") } else if AppInfo.isMainProcess() { // 主进程时,通过委托通知 DispatchQueue.main.async { delegate?.offlineTimerDidStop(track: track) FileLog.log(msg: "通过委托通知计时器停止 (时间段 > 60s): \(track.description)") } } } } OfflineTimerStoreHelper.updateStatus( changing: { old in if let o = old { finalEndTime = .now var elapsedSinceLastStart: Int64 = 0 if let runningStartTime = o.runingStartTime { elapsedSinceLastStart = max(0, Int64(Date.now.timeIntervalSince(runningStartTime))) } if o.timerMode == .countdown && reason == .completed && o.targetDurationSec != nil { // 倒计时完成的情况 finalAccumulatedSec = o.targetDurationSec! } else if o.timerMode == .countdown && (reason != .completed || o.targetDurationSec == nil) { // 倒计时但未完成或没有目标时间的情况 finalAccumulatedSec = o.accumulatedSec + elapsedSinceLastStart } else if o.timerMode == .positive { // 正计时情况 finalAccumulatedSec = o.accumulatedSec + elapsedSinceLastStart } return OfflineTimerStatus( type: o.type, state: .stoped, startTime: o.startTime, endTime: finalEndTime, runingStartTime: nil, accumulatedSec: finalAccumulatedSec, pauseCount: o.pauseCount, timerMode: o.timerMode, targetDurationSec: o.targetDurationSec, stopReason: reason ) } return nil }) if #available(iOS 17.0, *) { Task { await liveActivityManager?.endActivity() } } OfflineTimerNotification.cancelPauseNoti() onTimerPauseOrStop()

}

static func pauseIfRunningOnNonMain() {
if OfflineTimerStoreHelper.isRunning() {
pause()
}
}
}
修改

사고

Modifying code logic for timer

The user's message suggests that, in countdown mode, when the timer stops, it should check if the accumulated time exceeds the target time. If it does, the pause time may need to be adjusted to ensure the total does not exceed the target. They want the accumulated time to exactly match the target by modifying the code. I’ll need to adjust the logic for handling this and ensure that the segment pause time is correctly reduced to balance the total time.

Modifying stop() logic

The user has asked to modify the logic of the stop() function. Specifically, when the accumulated time plus the elapsed time exceeds the target, we need to adjust the current segment so that the total time doesn't exceed the target. This will involve clamping finalAccumulatedSec to targetDurationSec and modifying the track to reflect this shortened time.

I’ll explain the necessary changes in Chinese and modify the stop(reason:) and pause() logic accordingly.

Thought for 33 seconds

답변

下面给出 核心改动思路stop (reason:) 函数的完整替换代码
只要把该函数直接替换进 OfflineTimerHelper.swift 即可,其余文件无需改动。


设计要点

  1. 仅限倒计时模式 (.countdown)

    • 计算 rawTotal = accumulatedSec + elapsedSinceLastStart
    • rawTotal > targetDurationSec,说明本次最后一段超出了目标;
      • 将本段有效时长裁剪为 adjustedCurrent = targetDurationSec - accumulatedSec (可能为 0)。
      • 重新生成 TimeTrack,把 end 改为 runningStart + adjustedCurrent,保证记录不超时。
      • finalAccumulatedSec 固定为 targetDurationSec
  2. 正计时 (.positive) 或未超时的倒计时保持原逻辑。

  3. Live Activity & 通知 无需改动,只使用上面得到的 finalAccumulatedSec


替换后的 stop(reason:) 实现(Swift)

swift
/// > 更新 offline timer 状态为 stoped /// > 启动 limit/recharge monitor /// > 刷新 shield settings static func stop(reason: StopReason = .manual) { var finalAccumulatedSec: Int64 = 0 var finalEndTime: Date? = nil var stoppedTrack: TimeTrack? = nil OfflineTimerStoreHelper.updateStatus { old in guard let o = old else { return nil } let now = Date() var elapsedSinceLastStart: Int64 = 0 if let runningStart = o.runingStartTime { elapsedSinceLastStart = max(0, Int64(now.timeIntervalSince(runningStart))) } // ------------ ① 处理倒计时模式 ------------ if o.timerMode == .countdown, let target = o.targetDurationSec { let rawTotal = o.accumulatedSec + elapsedSinceLastStart if rawTotal > target { // 本次 running 段需要裁剪 let adjustedCurrent = max(0, target - o.accumulatedSec) finalAccumulatedSec = target // 保证总和恰好为目标 finalEndTime = o.runingStartTime?.addingTimeInterval( TimeInterval(adjustedCurrent) ) ?? now // 理论结束点 // 生成裁剪后的 track if let runningStart = o.runingStartTime { stoppedTrack = TimeTrack( start: runningStart, end: finalEndTime!, type: .offlineTimer, offlineType: o.type ) } } else if reason == .completed { finalAccumulatedSec = target finalEndTime = now if let runningStart = o.runingStartTime { stoppedTrack = TimeTrack( start: runningStart, end: now, type: .offlineTimer, offlineType: o.type ) } } else { finalAccumulatedSec = rawTotal finalEndTime = now if let runningStart = o.runingStartTime { stoppedTrack = TimeTrack( start: runningStart, end: now, type: .offlineTimer, offlineType: o.type ) } } // ------------ ② 正计时或其他情况 ------------ } else { finalAccumulatedSec = o.accumulatedSec + elapsedSinceLastStart finalEndTime = now if let runningStart = o.runingStartTime { stoppedTrack = TimeTrack( start: runningStart, end: now, type: .offlineTimer, offlineType: o.type ) } } // 保存满足「>60 s」的 track if let track = stoppedTrack, track.durationSec > 60 { if !AppInfo.isMainProcess() { TimeTrackStore.save(track: track).wait() } else { DispatchQueue.main.async { delegate?.offlineTimerDidStop(track: track) } } } return OfflineTimerStatus( type: o.type, state: .stoped, startTime: o.startTime, endTime: finalEndTime, runingStartTime: nil, accumulatedSec: finalAccumulatedSec, pauseCount: o.pauseCount, timerMode: o.timerMode, targetDurationSec: o.targetDurationSec, stopReason: reason ) } if #available(iOS 17.0, *) { Task { await liveActivityManager?.endActivity() } } OfflineTimerNotification.cancelPauseNoti() onTimerPauseOrStop() }

说明

  • 裁剪逻辑 只影响「最后一次 runningstop() 调用」这一段时间,不会修改之前已保存的暂停段。
  • 如果当次运行段本身不足以抵消超时(极端情况下 adjustedCurrent 为 0),就直接把本段 duration 记为 0,保证 finalAccumulatedSec 不超过目标。
  • 仍继承原有「> 60 s 才持久化 TimeTrack」的判断。

这样即可满足“倒计时模式停止时,若总时长超目标,则把本段运行时间按需削减,使记录总时长恰为目标”的需求。

이 Q&A 공유하기