计时器 倒计时模式下 如果 停止时检查本次运行积累总时间是否大于目标时间 如果大于目标时间 就讲...
बनाया गया: 9 मई 2025
बनाया गया: 9 मई 2025
计时器
倒计时模式下
如果 停止时检查本次运行积累总时间是否大于目标时间 如果大于目标时间
就讲本段落暂停时间段进行删减
确保 如果大于目标时间 本次记录的总时间是目标时间
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
}
textFileLog.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
textOfflineTimerStoreHelper.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
textOfflineTimerStoreHelper.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
textif 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()
}
}
}
修改
下面给出 核心改动思路 与 stop (reason:) 函数的完整替换代码。
只要把该函数直接替换进 OfflineTimerHelper.swift 即可,其余文件无需改动。
仅限倒计时模式 (.countdown)
rawTotal = accumulatedSec + elapsedSinceLastStart
。adjustedCurrent = targetDurationSec - accumulatedSec
(可能为 0)。TimeTrack
,把 end
改为 runningStart + adjustedCurrent
,保证记录不超时。finalAccumulatedSec
固定为 targetDurationSec
。正计时 (.positive) 或未超时的倒计时保持原逻辑。
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() }
running
到 stop()
调用」这一段时间,不会修改之前已保存的暂停段。duration
记为 0,保证 finalAccumulatedSec
不超过目标。这样即可满足“倒计时模式停止时,若总时长超目标,则把本段运行时间按需削减,使记录总时长恰为目标”的需求。