// Merged Swift Files // MARK: - utils/OfflineTi...

Erstellt am: 15. März 2025

Beantwortet mit O1 von Chat01

Frage

// Merged Swift Files

// MARK: - utils/OfflineTimer/LiveActivity/LiveActivityManager.swift

//
// File.swift
// utils
//
// Created by 潘令川 on 2025/3/11.
//

import ActivityKit
import Foundation

@available(iOS 16.1, *)
class LiveActivityManager {

text
static let shared = LiveActivityManager() private var currentActivity: Activity<OfflineTimerAttributes>? private init() {} func startActivity(timerName: String, emoji: String, timerType: String) -> Activity<OfflineTimerAttributes>? { // 先结束之前的活动 endActivity() var activity: Activity<OfflineTimerAttributes>? if #available(iOS 16.2, *) { do { let attributes = OfflineTimerAttributes( timerName: timerName, emoji: emoji ) let contentState = OfflineTimerAttributes.ContentState( elapsedTime: "0:00", elapsedSeconds: 0, timerType: timerType, timerState: "running" ) let content = ActivityContent(state: contentState, staleDate: nil) activity = try Activity<OfflineTimerAttributes>.request( attributes: attributes, content: content, pushType: nil ) currentActivity = activity print("开始离线计时器活动: \(activity?.id ?? "unknown")") } catch { print("创建活动失败: \(error.localizedDescription)") } }

// else if #available(iOS 16.1, *) {
// do {
// let attributes = OfflineTimerAttributes(
// timerName: timerName,
// emoji: emoji
// )
//
// let contentState = OfflineTimerAttributes.ContentState(
// elapsedTime: "0:00",
// elapsedSeconds: 0,
// timerType: timerType,
// timerState: "running"
// )
//
// activity = try Activity<OfflineTimerAttributes>.request(
// attributes: attributes,
// contentState: contentState,
// pushType: nil
// )
//
// currentActivity = activity
//
// print("开始离线计时器活动: (activity?.id ?? "unknown")")
// } catch {
// print("创建活动失败: (error.localizedDescription)")
// }
// }

text
return activity }

func updateActivity(elapsedSeconds: Int64, isPaused: Bool = false) {
// print("更新 LiveActivity: 时间 = (elapsedSeconds), 暂停状态 = (isPaused)")
// FileLog.log(msg: "更新 LiveActivity: 时间 = (elapsedSeconds), 暂停状态 = (isPaused)")

text
if #available(iOS 16.2, *) { Task { let contentState = OfflineTimerAttributes.ContentState( elapsedTime: formatSimpleTime(elapsedSeconds), elapsedSeconds: elapsedSeconds, timerType: currentActivity?.content.state.timerType ?? "", timerState: isPaused ? "paused" : "running" ) let content = ActivityContent(state: contentState, staleDate: nil) if let activity = currentActivity { await activity.update(content) } else { print("无法更新 LiveActivity: 当前活动为空") FileLog.log(msg: "无法更新 LiveActivity: 当前活动为空") } } }

// else if #available(iOS 16.1, *) {
// Task {
// let contentState = OfflineTimerAttributes.ContentState(
// elapsedTime: formatSimpleTime(elapsedSeconds),
// elapsedSeconds: elapsedSeconds,
// timerType: currentActivity?.contentState.timerType ?? "",
// timerState: isPaused ? "paused" : "running"
// )
//
// if let activity = currentActivity {
// print("正在更新 LiveActivity ID: (activity.id)")
// FileLog.log(msg: "正在更新 LiveActivity ID: (activity.id), 状态: (isPaused ? "paused" : "running")")
// await activity.update(using: contentState)
// print("LiveActivity 更新完成")
// FileLog.log(msg: "LiveActivity 更新完成")
// } else {
// print("无法更新 LiveActivity: 当前活动为空")
// FileLog.log(msg: "无法更新 LiveActivity: 当前活动为空")
// }
// }
// }
}

text
func endActivity() { if #available(iOS 16.2, *) { Task { for activity in Activity<OfflineTimerAttributes>.activities { let finalContent = ActivityContent( state: activity.content.state, staleDate: nil ) await activity.end(finalContent, dismissalPolicy: .immediate) } currentActivity = nil } } // else if #available(iOS 16.1, *) { // Task { // for activity in Activity<OfflineTimerAttributes>.activities { // await activity.end(dismissalPolicy: .immediate) // } // currentActivity = nil // } // } }

}

// 扩展 Int64 以格式化时间
extension Int64 {
func format(using units: NSCalendar.Unit) -> String {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = units
formatter.unitsStyle = .positional
formatter.zeroFormattingBehavior = .pad
return formatter.string(from: TimeInterval(self)) ?? "00:00:00"
}
}

// 简化的时间格式化函数,只显示分钟和秒,格式为"6:32"
private func formatSimpleTime(_ seconds: Int64) -> String {
let minutes = seconds / 60
let remainingSeconds = seconds % 60
return "(minutes):(String(format: "%02d", remainingSeconds))"
}

// MARK: - utils/OfflineTimer/LiveActivity/OfflineTimerAttributes.swift

import ActivityKit

import SwiftUI

public struct OfflineTimerAttributes: ActivityAttributes {

text
public struct ContentState: Codable, Hashable { // 动态属性,可以在活动创建后更新 public var elapsedTime: String // 已经过的时间,格式化为字符串 public var elapsedSeconds: Int64 // 已经过的秒数 public var timerType: String // 计时器类型(如健身、学习等) public var timerState: String // 计时器状态(running, paused, stopped) public init(elapsedTime: String, elapsedSeconds: Int64, timerType: String, timerState: String) { self.elapsedTime = elapsedTime self.elapsedSeconds = elapsedSeconds self.timerType = timerType self.timerState = timerState } } // 静态属性,只在创建活动时使用 public var timerName: String // 计时器名称 public var emoji: String // 计时器类型对应的表情符号 public init(timerName: String, emoji: String) { self.timerName = timerName self.emoji = emoji }

}

// MARK: - utils/OfflineTimer/LiveActivity/OfflineTimerIntent.swift

import AppIntents
import SwiftUI

@available(iOS 16.1, *)
public struct PauseTimerIntent: AppIntent, LiveActivityIntent {
public static var title: LocalizedStringResource = "暂停计时器"
public static var description = IntentDescription("暂停离线计时器")

text
public init() {} public static var openAppWhenRun: Bool { false } public func perform() async throws -> some IntentResult {

// print("灵动岛执行暂停操作")
// FileLog.log(msg: "灵动岛执行暂停操作")

text
await MainActor.run { OfflineTimerHelper.pause(suspend: false) NotificationCenter.default.post(name: .offlineTimerStatusChange, object: OfflineTimerStoreHelper.getStatus()) } return .result() }

}

@available(iOS 16.1, *)
public struct ResumeTimerIntent: AppIntent, LiveActivityIntent {
public static var title: LocalizedStringResource = "继续计时器"
public static var description = IntentDescription("继续离线计时器")

text
public init() {} public static var openAppWhenRun: Bool { false } public func perform() async throws -> some IntentResult { // print("灵动岛执行继续操作") // FileLog.log(msg: "灵动岛执行继续操作") await MainActor.run { OfflineTimerHelper.resume() NotificationCenter.default.post(name: .offlineTimerStatusChange, object: OfflineTimerStoreHelper.getStatus()) } return .result() }

}

@available(iOS 16.1, *)
public struct StopTimerIntent: AppIntent, LiveActivityIntent {
public static var title: LocalizedStringResource = "停止计时器"
public static var description = IntentDescription("停止离线计时器")

text
public init() {} public static var openAppWhenRun: Bool { false } public func perform() async throws -> some IntentResult { // print("灵动岛执行停止操作") // FileLog.log(msg: "灵动岛执行停止操作") await MainActor.run { OfflineTimerHelper.stop() NotificationCenter.default.post(name: .offlineTimerStatusChange, object: OfflineTimerStoreHelper.getStatus()) } return .result() }

}

@available(iOS 16.1, *)
public struct OfflineTimerIntents: AppIntentsPackage {
public static var appIntents: [any AppIntent.Type] {
[PauseTimerIntent.self, ResumeTimerIntent.self, StopTimerIntent.self]
}
}

// MARK: - utils/OfflineTimer/LiveActivity/OfflineTimerWidget.swift

import WidgetKit
import SwiftUI
import ActivityKit
import AppIntents

@available(iOS 16.1, *)
struct OfflineTimerWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: OfflineTimerAttributes.self) { context in
// 锁屏界面
LockScreenView(context: context)
} dynamicIsland: { context in
DynamicIsland {
// 展开区域
DynamicIslandExpandedRegion(.leading) {
LeadingView(context: context)
}

text
DynamicIslandExpandedRegion(.center) { CenterView(context: context) } DynamicIslandExpandedRegion(.trailing) { TrailingView(context: context) } DynamicIslandExpandedRegion(.bottom) {

// BottomView(context: context)
}
} compactLeading: {
CompactLeadingView(context: context)
} compactTrailing: {
CompactTrailingView(context: context)
} minimal: {
MinimalView(context: context)
}
}
}

text
private func formatSeconds(_ seconds: Int64) -> String { let formatter = DateComponentsFormatter() formatter.allowedUnits = [.hour, .minute, .second] formatter.unitsStyle = .positional formatter.zeroFormattingBehavior = .pad return formatter.string(from: TimeInterval(seconds)) ?? "00:00:00" }

}

@available(iOS 16.1, *)
private struct LockScreenView: View {
let context: ActivityViewContext<OfflineTimerAttributes>

text
var body: some View { ZStack { RoundedRectangle(cornerRadius: 24, style: .continuous) .foregroundStyle(Color.black) HStack(spacing: 0) { VStack(alignment: .leading,spacing:16){ Text(NSLocalizedString("offline_timer", comment: "")) .font(.system(size: 14, weight: .semibold, design: .rounded)) .foregroundColor(Color(hex: "#67C9FF")) VStack(alignment: .center, spacing: 2) { Text(context.attributes.emoji) .font(.system(size: 56, design: .rounded)) Text(context.attributes.timerName) .font(.system(size: 14, weight: .regular, design: .rounded)) .foregroundColor(Color(hex: "#B1BCCB")) } } .padding(.horizontal, 16) .padding(.leading, 20) Spacer() VStack(alignment: .trailing, spacing:14){ // 大号时间显示 - 已经过时间 VStack(alignment: .trailing, spacing: 2) { // 根据时间长度调整字体大小和格式 if context.state.elapsedSeconds >= 36000 { // 10小时或以上 Text(formatLongTime(context.state.elapsedSeconds)) .font(.system(size: 52, weight: .semibold, design: .rounded)) .monospacedDigit() .foregroundColor(.white) } else { Text(context.state.elapsedTime) .font(.system(size: 56, weight: .semibold, design: .rounded)) .monospacedDigit() .foregroundColor(.white) } // 添加暂停状态显示 if context.state.timerState == "paused" { Text(NSLocalizedString("Paused", comment: "Paused state")) .font(.system(size: 14, weight: .medium, design: .rounded)) .foregroundColor(.white) } } // 控制按钮 HStack(spacing: 8) { if #available(iOS 17.0, *) { // 使用专用Intent处理按钮交互 let isPaused = context.state.timerState == "paused" if isPaused { Button(intent: ResumeTimerIntent()) { Capsule() .fill(Color(hex: "#67C9FF")) .frame(width: 60, height: 38) .overlay( Image(systemName: "play.fill") .font(.system(size: 20, design: .rounded)) .foregroundColor(Color(hex: "#031C3E")) ) } .buttonStyle(.plain) .contentShape(Rectangle()) } else { Button(intent: PauseTimerIntent()) { Capsule() .fill(Color(hex: "#67C9FF")) .frame(width: 60, height: 38) .overlay( Image(systemName: "pause.fill") .font(.system(size: 20, design: .rounded)) .foregroundColor(Color(hex: "#031C3E")) ) } .buttonStyle(.plain) .contentShape(Rectangle()) } Button(intent: StopTimerIntent()) { Capsule() .fill(Color(hex: "#F65252")) .frame(width: 60, height: 38) .overlay( Image(systemName: "stop.fill") .font(.system(size: 20, design: .rounded)) .foregroundColor(Color(hex: "#031C3E")) ) } .buttonStyle(.plain) .contentShape(Rectangle()) } } } .padding(16) } } } // 格式化长时间显示 (10小时以上) private func formatLongTime(_ seconds: Int64) -> String { let hours = seconds / 3600 let minutes = (seconds % 3600) / 60 return "\(hours)h\(String(format: "%02d", minutes))m" }

}

@available(iOS 16.1, *)
private struct LeadingView: View {
let context: ActivityViewContext<OfflineTimerAttributes>

text
var body: some View { VStack(alignment: .leading, spacing: 14) { Text(NSLocalizedString("offline_timer", comment: "")) .font(.system(size: 14, weight: .semibold, design: .rounded)) .foregroundColor(Color(hex: "#67C9FF")) .fixedSize(horizontal: true, vertical: false) VStack(alignment: .center, spacing: 2) { Text(context.attributes.emoji) .font(.system(size: 42, design: .rounded)) Text(context.attributes.timerName) .font(.system(size: 14, weight: .regular, design: .rounded)) .foregroundColor(Color(hex: "#B1BCCB")) } } .padding(.leading, 8) }

}

@available(iOS 16.1, *)
private struct CenterView: View {
let context: ActivityViewContext<OfflineTimerAttributes>

text
var body: some View { VStack(spacing: 2) { // 根据时间长度调整字体大小和格式 if context.state.elapsedSeconds >= 36000 { // 10小时或以上 Text(formatLongTime(context.state.elapsedSeconds)) .font(.system(size: 52, weight: .semibold, design: .rounded)) .monospacedDigit() .contentTransition(.numericText()) .fixedSize(horizontal: true, vertical: false) } else { Text(context.state.elapsedTime) .font(.system(size: 72, weight: .semibold, design: .rounded)) .monospacedDigit() .contentTransition(.numericText()) .fixedSize(horizontal: true, vertical: false) } // 添加暂停状态显示 if context.state.timerState == "paused" { Text(NSLocalizedString("Paused", comment: "Paused state")) .font(.system(size: 16, weight: .medium, design: .rounded)) .foregroundColor(.white) } } } // 格式化长时间显示 (10小时以上) private func formatLongTime(_ seconds: Int64) -> String { let hours = seconds / 3600 let minutes = (seconds % 3600) / 60 return "\(hours)h\(String(format: "%02d", minutes))m" }

}

@available(iOS 16.1, *)
private struct TrailingView: View {
let context: ActivityViewContext<OfflineTimerAttributes>

text
var body: some View { VStack(spacing:14){ if #available(iOS 17.0, *) { // 使用专用Intent处理按钮交互 let isPaused = context.state.timerState == "paused" if isPaused { Button(intent: ResumeTimerIntent()) { Circle() .fill(Color(hex: "#67C9FF")) .frame(width: 48, height: 48) .overlay( Image(systemName: "play.fill") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 18, height: 18) .foregroundColor(Color(hex: "#031C3E")) ) } .buttonStyle(.plain) .contentShape(Rectangle()) } else { Button(intent: PauseTimerIntent()) { Circle() .fill(Color(hex: "#67C9FF")) .frame(width: 48, height: 48) .overlay( Image(systemName: "pause.fill") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 18, height: 18) .foregroundColor(Color(hex: "#031C3E")) ) } .buttonStyle(.plain) .contentShape(Rectangle()) } Button(intent: StopTimerIntent()) { Circle() .fill(Color(hex: "#F65252")) .frame(width: 48, height: 48) .overlay( Image(systemName: "stop.fill") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 18, height: 18) .foregroundColor(Color(hex: "#031C3E")) ) } .buttonStyle(.plain) .contentShape(Rectangle()) } } .padding(.trailing,8) }

}

//@available(iOS 16.1, *)
//private struct BottomView: View {
// let context: ActivityViewContext<OfflineTimerAttributes>
//
// var body: some View {
// if #available(iOS 17.0, *) {
// Button(intent: StopTimerIntent()) {
// Circle()
// .fill(Color.red)
// .frame(width: 40, height: 40)
// .overlay(
// Image(systemName: "stop.fill")
// .font(.title3)
// .foregroundColor(.white)
// )
// }
// .buttonStyle(.plain)
// .contentShape(Rectangle())
// .padding(.bottom, 8)
// }
// }
//}

@available(iOS 16.1, *)
private struct CompactLeadingView: View {
let context: ActivityViewContext<OfflineTimerAttributes>

text
var body: some View { Image("offline_timer") .font(.system(size: 22, design: .rounded)) .foregroundColor(Color(hex: "#67C9FF"))

// .foregroundColor(Color.color_chart_tint_accentTurquoise_bar_primary_solid)

}

}

@available(iOS 16.1, *)
private struct CompactTrailingView: View {
let context: ActivityViewContext<OfflineTimerAttributes>

text
var body: some View { HStack(spacing: 4) { Text(formatTimeShort(context.state.elapsedSeconds)) .font(.system(size: 18, weight: .semibold, design: .rounded)) .monospacedDigit() .foregroundColor(.primary) .lineLimit(1) .minimumScaleFactor(0.8) // 添加暂停状态图标 if context.state.timerState == "paused" { Image(systemName: "pause.fill") .font(.system(size: 15, weight: .semibold)) .foregroundColor(.white) } } .padding(.trailing, 4) } private func formatTimeShort(_ seconds: Int64) -> String { let hours = seconds / 3600 let minutes = (seconds % 3600) / 60 if hours > 0 { return "\(hours):\(String(format: "%02d", minutes))" } else { return "\(minutes):\(String(format: "%02d", seconds % 60))" } }

}

@available(iOS 16.1, *)
private struct MinimalView: View {
let context: ActivityViewContext<OfflineTimerAttributes>

text
var body: some View { VStack(spacing: 2) { Text(NSLocalizedString("TIMER", comment: "Title for timer feature")) .font(.system(size: 20, weight: .semibold, design: .rounded)) .foregroundColor(Color(hex: "#67C9FF")) Text(formatMinimalTime(context.state.elapsedSeconds)) .font(.system(size: 15, weight: .semibold, design: .rounded)) .monospacedDigit() // 添加暂停状态显示 if context.state.timerState == "paused" { Text(NSLocalizedString("Paused", comment: "Paused state")) .font(.system(size: 10, weight: .medium, design: .rounded)) .foregroundColor(Color(hex: "#B1BCCB")) } } } private func formatMinimalTime(_ seconds: Int64) -> String { let hours = Double(seconds) / 3600.0 let minutes = seconds / 60 if hours >= 1.0 { // 高于1小时,显示为小时格式,保留1位小数 return String(format: "%.1f%@", hours, NSLocalizedString("h", comment: "Hour unit")) } else { // 低于1小时,显示为分钟格式 return "\(minutes)\(NSLocalizedString("min", comment: "Minute unit"))" } }

}

// 添加格式化时间的辅助函数
private func formatTimeShort(_ seconds: Int64) -> String {
let hours = seconds / 3600
let minutes = (seconds % 3600) / 60

text
if hours > 0 { return "\(hours):\(String(format: "%02d", minutes))" } else { return "\(minutes):\(String(format: "%02d", seconds % 60))" }

}

extension Color {
init(hex: String) {
var hexSanitized = hex.trimmingCharacters(in: .whitespacesAndNewlines)
hexSanitized = hexSanitized.replacingOccurrences(of: "#", with: "")

text
var rgb: UInt64 = 0 Scanner(string: hexSanitized).scanHexInt64(&rgb) let red = Double((rgb & 0xFF0000) >> 16) / 255.0 let green = Double((rgb & 0x00FF00) >> 8) / 255.0 let blue = Double(rgb & 0x0000FF) / 255.0 self.init(red: red, green: green, blue: blue) }

}

// MARK: - utils/OfflineTimer/OfflineTimerHelper.swift

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

import Foundation
import DeviceActivity
import ActivityKit

class OfflineTimerHelper {

text
private static let deviceCenter = DeviceActivityCenter() private static var liveActivityUpdateTimer: Timer? static let emoji = "⌚️" // > 更新 offline timer // > 停掉 limit/recharge 的 monitor // > shield掉所有app static func start(type: OfflineTimerType) { FileLog.log(msg: "\(emoji) timer start \(type.emoji)") let status = OfflineTimerStatus(type: type, state: .running, startTime: .now, endTime: nil, runingStartTime: .now, accumulatedSec: 0, pauseCount: 0) OfflineTimerStoreHelper.saveStatus(status) stopOtherMonitorsShieldAllAndStartTimer(type: type) // 启动灵动岛 if #available(iOS 16.1, *) { startLiveActivity(type: type) startLiveActivityUpdateTimer() } } 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(suspend: Bool) { FileLog.log(msg: "\(emoji) timer pause") OfflineTimerStoreHelper.updateStatus(changing: { old in if let status = old, let runingStartTime = status.runingStartTime { let totalSec = max(Int64(0), Int64(Date.now.timeIntervalSince(runingStartTime))) + status.accumulatedSec if (suspend) { // 当 suspend = false 时,说明时主程序调用,此时直接存到db里,不需要存到 store 里 if (Date.now.timeIntervalSince(runingStartTime) > 60) { TimeTrackStore.save(track: TimeTrack(start: runingStartTime, end: .now, type: .offlineTimer, offlineType: status.type)).wait() } } return OfflineTimerStatus(type: status.type, state: .paused, startTime: status.startTime, endTime: nil, runingStartTime: nil, accumulatedSec: totalSec, pauseCount: status.pauseCount + 1) } return nil }) // 确保UserDefaults同步 StoreHelper.userDefaults.synchronize() onTimerPauseOrStop(suspend: suspend) // 更新灵动岛状态 if #available(iOS 16.1, *) { updateLiveActivity() stopLiveActivityUpdateTimer() } // 添加通知,确保 UI 更新 NotificationCenter.default.post(name: .offlineTimerStatusChange, object: OfflineTimerStoreHelper.getStatus()) } private static func onTimerPauseOrStop(suspend: Bool) { 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? OfflineTimerStoreHelper.updateStatus(changing: { old in if let status = old { type = status.type return OfflineTimerStatus(type: status.type, state: .running, startTime: status.startTime, endTime: nil, runingStartTime: .now, accumulatedSec: status.accumulatedSec, pauseCount: status.pauseCount) } return nil }) // 确保UserDefaults同步 StoreHelper.userDefaults.synchronize() if let t = type { stopOtherMonitorsShieldAllAndStartTimer(type: t) } // 更新灵动岛状态 if #available(iOS 16.1, *) { updateLiveActivity() startLiveActivityUpdateTimer() } // 添加通知,确保 UI 更新 NotificationCenter.default.post(name: .offlineTimerStatusChange, object: OfflineTimerStoreHelper.getStatus()) } // > 更新 offline timer 状态为 stoped,等待用户下次打开app时存到db // > 启动 limit/recharge monitor // > 刷新 shield settings static func stop() { OfflineTimerStoreHelper.updateStatus(changing: { old in if let o = old { var totalSec = o.accumulatedSec if let runingStartTime = o.runingStartTime { totalSec += max(Int64(0), Int64(Date.now.timeIntervalSince(runingStartTime))) } return OfflineTimerStatus(type: o.type, state: .stoped, startTime: o.startTime, endTime: .now, runingStartTime: nil, accumulatedSec: totalSec, pauseCount: o.pauseCount) } return nil }) // 确保UserDefaults同步 StoreHelper.userDefaults.synchronize() onTimerPauseOrStop(suspend: false) // 结束灵动岛 if #available(iOS 16.1, *) { endLiveActivity() stopLiveActivityUpdateTimer() } // 添加通知,确保 UI 更新 NotificationCenter.default.post(name: .offlineTimerStatusChange, object: OfflineTimerStoreHelper.getStatus()) } static func pauseIfRunningOnNonMain() { if OfflineTimerStoreHelper.isRunning() { pause(suspend: true) } } // MARK: - 灵动岛相关方法 @available(iOS 16.1, *) private static func startLiveActivity(type: OfflineTimerType) { if let status = OfflineTimerStoreHelper.getStatus() { // 启动灵动岛活动 LiveActivityManager.shared.startActivity( timerName: type.title, emoji: type.emoji, timerType: type.rawValue ) } } @available(iOS 16.1, *) private static func updateLiveActivity() { if let status = OfflineTimerStoreHelper.getStatus() { var elapsedSeconds = status.accumulatedSec // 如果计时器正在运行,需要加上当前运行的时间 if status.state == .running, let runingStartTime = status.runingStartTime { elapsedSeconds += Int64(Date.now.timeIntervalSince(runingStartTime)) } // 更新灵动岛活动,传递暂停状态 LiveActivityManager.shared.updateActivity( elapsedSeconds: elapsedSeconds, isPaused: status.state == .paused ) } else { print("OfflineTimerHelper: 无法更新LiveActivity - 状态为空") FileLog.log(msg: "OfflineTimerHelper: 无法更新LiveActivity - 状态为空") } } @available(iOS 16.1, *) private static func endLiveActivity() { // 结束灵动岛活动 LiveActivityManager.shared.endActivity() } // MARK: - 灵动岛更新计时器 @available(iOS 16.1, *) private static func startLiveActivityUpdateTimer() { // 停止已有的计时器 stopLiveActivityUpdateTimer() // 创建新的计时器,每秒更新一次灵动岛 liveActivityUpdateTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in updateLiveActivity() } } @available(iOS 16.1, *) private static func stopLiveActivityUpdateTimer() { liveActivityUpdateTimer?.invalidate() liveActivityUpdateTimer = nil } // 添加初始化通知监听的方法 static func setupNotificationObserver() { print("设置离线计时器通知监听器") // 确保在主线程上执行 if !Thread.isMainThread { DispatchQueue.main.async { setupNotificationObserver() } return } // 由于我们现在直接在AppIntent中调用相应方法,不再需要通知监听器 // 但为了保持代码的完整性,我们保留这个方法 print("离线计时器通知监听器设置完成") } // 处理按钮操作的方法 private static func handleAction(_ action: String) { print("开始处理离线计时器操作: \(action)") switch action { case "pause": print("执行暂停操作") pause(suspend: false) FileLog.log(msg: "灵动岛触发暂停操作") case "resume": print("执行继续操作") resume() FileLog.log(msg: "灵动岛触发继续操作") case "stop": print("执行停止操作") stop() FileLog.log(msg: "灵动岛触发停止操作") default: print("未知操作: \(action)") FileLog.log(msg: "灵动岛触发未知操作: \(action)") } print("离线计时器操作处理完成: \(action)") } // 在应用启动时调用setupNotificationObserver static func initialize() { print("初始化离线计时器助手") // 设置通知监听器 setupNotificationObserver() // 其他初始化代码... print("离线计时器助手初始化完成") }

}

// MARK: - utils/OfflineTimer/OfflineTimerRedeemInfo.swift

//
// OfflineTimerRedeemInfo.swift
// TimeMiner
//
// Created by 张磊 on 2024/4/26.
//

import Foundation

// 当天的离线计时总时间 = offlineTimer.db 中endTime在今天的所有数据加起来的和
//
struct OfflineTimerRedeemInfo: Codable {
var costTimerMins: Int // 消耗的 offline timer 分钟
var redeemedMins: Int // 获得了多少分钟
}

// MARK: - utils/OfflineTimer/OfflineTimerStatus.swift

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

import Foundation

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

text
let startTime: Date // 开始时点 let endTime: Date? // 停止时点 let runingStartTime: Date? // 从哪个时点开始的,只有当 type == running 时才有意义 let accumulatedSec: Int64 // 累计的秒数,不包含正在进行的 let pauseCount: Int var description: String { return "\(type)-\(state) (\(startTime.secStr())-\(endTime?.secStr() ?? "?")) running(\(runingStartTime?.secStr() ?? "?")) accumulatedSec \(accumulatedSec) pauseCount \(pauseCount)" } func getOfflineDurSec() -> Int64 { if let status = OfflineTimerStoreHelper.getStatus() { var durSec = status.accumulatedSec if status.state == .running, let runingStartTime = status.runingStartTime { durSec += Int64(Date.now.timeIntervalSince(runingStartTime)) } return durSec } return 0 }

}

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

// MARK: - utils/OfflineTimer/OfflineTimerStoreHelper.swift

//
// OfflineTimerStoreHelper.swift
// TimeMiner
//
// Created by 张磊 on 2024/4/26.
//

import Foundation
import Combine

class OfflineTimerStoreHelper {

text
static let statusFlow = CurrentValueSubject<OfflineTimerStatus?, Never>(nil) static func loadRedeemInfo() -> OfflineTimerRedeemInfo? { if let savedData = StoreHelper.userDefaults.data(forKey: StoreKeys.offline_timer_redeem_info) { let decoder = JSONDecoder() if let loadedData = try? decoder.decode(OfflineTimerRedeemInfo.self, from: savedData) { return loadedData } } return nil } static func saveRedeemInfo(_ data: OfflineTimerRedeemInfo) { let encoder = JSONEncoder() if let encodedData = try? encoder.encode(data) { StoreHelper.userDefaults.set(encodedData, forKey: StoreKeys.offline_timer_redeem_info) StoreHelper.userDefaults.synchronize() DispatchQueue.main.async { NotificationCenter.default.post(name: .offlineTimerRedeemInfoChange, object: data) } } } static func getTotalTime() -> Int { return StoreHelper.getInt(key: StoreKeys.total_offline_timer_mins) } static func getRemainTime() -> Int { let totalMins = getTotalTime() let info = loadRedeemInfo() return max(0, totalMins - (info?.costTimerMins ?? 0)) } static func setTotalTime(mins: Int) { StoreHelper.saveInt(key: StoreKeys.total_offline_timer_mins, value: mins) DispatchQueue.main.async { NotificationCenter.default.post(name: .offlineTimerTotalMinsChange, object: mins) } } static func totalMinsAdd(mins: Int) { setTotalTime(mins: getTotalTime() + mins) } static func getStatus() -> OfflineTimerStatus? { if let savedData = StoreHelper.userDefaults.data(forKey: StoreKeys.offline_timer_status) { let decoder = JSONDecoder() if let loadedData = try? decoder.decode(OfflineTimerStatus.self, from: savedData) { return loadedData } } return nil } static func isRunning() -> Bool { return getStatus()?.state == .running } static func updateStatus(changing: (OfflineTimerStatus?) -> OfflineTimerStatus?) { FileLog.log(msg: "OfflineTimerStoreHelper: 开始更新状态") let oldStatus = getStatus() FileLog.log(msg: "OfflineTimerStoreHelper: 旧状态 = \(oldStatus?.description ?? "nil")") if let newStatus = changing(oldStatus) { saveStatus(newStatus) FileLog.log(msg: "OfflineTimerStoreHelper: 状态已更新为 = \(newStatus.description)") } else { FileLog.log(msg: "OfflineTimerStoreHelper: 状态更新失败 - 新状态为nil") } } // 保存数据到UserDefaults static func saveStatus(_ data: OfflineTimerStatus) { let encoder = JSONEncoder() if let encodedData = try? encoder.encode(data) { StoreHelper.userDefaults.set(encodedData, forKey: StoreKeys.offline_timer_status) StoreHelper.userDefaults.synchronize() FileLog.log(msg: "OfflineTimerStoreHelper: 状态已保存到UserDefaults") DispatchQueue.main.async { NotificationCenter.default.post(name: .offlineTimerStatusChange, object: data) FileLog.log(msg: "OfflineTimerStoreHelper: 已发送状态变更通知") } } else { FileLog.log(msg: "OfflineTimerStoreHelper: 状态保存失败 - 编码错误") } print("save status \(data.description)") statusFlow.send(data) } static func clearStatus() { StoreHelper.clearKeys(keys: [StoreKeys.offline_timer_status]) statusFlow.send(nil) }

}

// MARK: - utils/OfflineTimer/OfflineTimerType.swift

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

import Foundation

enum OfflineTimerType: String, Codable {

text
case study // 学习 case work // 工作 case read // 读书 case fitness // 健身 case meditate // 冥想 static var defaultValue: OfflineTimerType { return .meditate } var emoji: String { switch(self) { case .meditate: return "🧘" case .study: return "🎓" case .work: return "🧑‍💻" case .read: return "📚" case .fitness: return "🏋️" } } var title: String { switch(self) { case .meditate: return NSLocalizedString("mediate", comment: "") case .work: return NSLocalizedString("work", comment: "") case .read: return NSLocalizedString("read", comment: "") case .fitness: return NSLocalizedString("fitness", comment: "") case .study: return NSLocalizedString("study", comment: "") } } var blockTitle: String { return "\(title)\n\(NSLocalizedString("offline_timing", comment: ""))" } var blockSubtitle: String { return NSLocalizedString("offline_timer_subtitle", comment: "") } var primaryBtnText: String { return NSLocalizedString("block_continue_offline", comment: "") } var secondaryBtnText: String { return NSLocalizedString("pause_offline", comment: "") }

}

这是优化的具体思路 : 但是内部部分已经完成
内部计时逻辑:由应用内部负责记录开始、结束、暂停等时间点,并存储至 UserDefaults,即使应用退出也能保证数据不会丢失。这种方案可以确保计时状态的持续性和正确性。

外部显示与同步:利用灵动岛的 Text(timerInterval:) 来展示计时状态,依靠系统级的时间计算来保持显示的实时更新,这样即使应用进程被暂停或者退出,计时器的显示依然能保持正确的时间显示。

AppIntent 交互:使用 AppIntent 来实现内外部状态同步,通过与应用内部的数据交互更新状态(例如暂停、继续、中止),可以使灵动岛上的显示与应用内部计时逻辑保持一致。

我们主要修改的是 利用灵动岛的 Text(timerInterval:) 来展示计时状态,依靠系统级的时间计算来保持显示的实时更新,这样即使应用进程被暂停或者退出,计时器的显示依然能保持正确的时间显示。

但是还有很多细节需要仔细审查 辨别 给我详细的修改思路
先不要写代码先思考
请一步一步的给我详细的思考链和具体步骤的解析

Gedanke

Integrating timer functionality

OK, let's think this through: Part 1 focuses on using internal application logic to track and save timer states. Part 2 highlights leveraging Dynamic Island's visual capabilities for real-time status updates.

Focusing on logical steps

I’m thinking about a structured approach to effectively combine ActivityKit, Dynamic Island, and AppIntent, while clearly connecting each aspect without revealing the chain of thought.

Charting the course

Piecing together a streamlined process, I highlight using Text(timerInterval:) for timers, maintaining internal state, managing pauses, transitioning to system updates, and adhering to version specifics.

Reasoned for 6 seconds

Antwort

下面给出一个比较系统且循序渐进的思考流程,帮助你从“内部计时逻辑”到“Live Activity 展示”再到“AppIntent 交互”一步步梳理如何将灵动岛的计时显示与应用内部的离线计时逻辑整合起来,同时利用系统级 Text(timerInterval:) 来自动刷新 UI。以下步骤并没有直接给出实现代码,而是着重强调思路、原理与流程。


1. 明确需求与目标

  1. 内部计时逻辑

    • 通过应用内部来管理“开始时间”、“暂停时间”、“累积时长”等核心数据,存储于 UserDefaults 或数据库,以保证当应用被杀进程或系统回收后也能持久化这些状态。
    • 当重新启动或恢复 App 时,可以读取并延续上次的计时状态。
  2. 外部展示与自动刷新

    • 在 iOS 16.2+ 中,ActivityKit 提供了 Text(timerInterval:) 这种基于系统级别的计时控件。它可以根据一个指定的开始时间和结束时间,自动进行实时的倒计时或正计时渲染,而无需我们自己通过定时器去 update() Live Activity。
    • 这种方式可以在 App 暂停或者被杀死进程后依旧继续进行动画或文本的实时更新,尤其适合做计时器类的场景。
  3. AppIntent 交互

    • 当用户在灵动岛或锁屏界面执行“暂停”、“继续”或“停止”操作时,需要触发相应的 AppIntent,以便让应用内部能够同步更新状态。
    • 借此可保证灵动岛上的交互动作能立刻反映到应用内部持久化的逻辑,从而两边数据保持一致。

2. 内部计时与系统计时的配合方式

2.1 为什么要同时保留“内部计时”与“系统级计时”?

  • 内部计时能保证在各种条件下(比如用户多次杀进程、重启设备等)都能持久化计时状态,并进行精确的统计。因为 Live Activity 并不是一个持久化逻辑容器,系统可能在一些极端情况下停止或回收它。
  • 系统级计时(即 timerInterval)能让我们在 UI 层级上不必每秒手动去 update(), 避免在后台时被系统限制更新频率,从而在锁屏和灵动岛始终显示准确的计时信息。

2.2 “正向计时” vs “倒计时”

  • Text(timerInterval: startDate...endDate, countsDown: false) 这种形式可以做正向计时(从 startDateendDate 之间显示经过的时间)。
  • 如果你想要做“无固定结束时间”的计时,往往需要将 endDate 设得非常久远(例如几小时甚至几天后),或者动态更新 endDate
  • 当需要“暂停/继续”时,可以通过中断或者改变 timerInterval 乃至 countsDown 的值实现不同的效果。

3. 活动的启动与结束:属性设计

3.1 Attributes 中如何保存 startTime

如果要用到 Text(timerInterval:),通常需要把计时开始的时间(以及预计结束的时间)放进你的 ActivityAttributes.ContentState 或者 ActivityAttributes 中。

  • 比如你可以在 OfflineTimerAttributes.ContentState 中增加一个字段:var startDate: Date
  • 在开始计时时设置 startDate = Date();如果你有结束时间的概念,也可以加 var endDate: Date?

3.2 不要完全依赖 elapsedSeconds

在你原先的逻辑里,你通过每秒计算并手动更新 elapsedSeconds。如果要让系统自动刷新 Live Activity,不需要再手动每秒推送更新内容,而是把实际的 “开始时间” 和 “结束时间” 或者 “暂停时间” 告诉系统,令其自行绘制。


4. 利用 Text(timerInterval:) 的关键点

4.1 基本用法

在 Live Activity 的视图里,你可以写类似:

swift
Text(timerInterval: state.startDate...Date(), countsDown: false)
  • 这表示从 state.startDate 到当前时间进行正计时。
  • 其中 Date() 在 Live Activity 的 Widget Extension 中可以直接拿到“当前时间”吗?其实系统会自动帮你进行流动性的渲染,你可以只在初次渲染时提供一个 Date.distantFuture 做上限,或者在 ContentState 里也放一个“动态计算的 endDate”。
  • countsDown 设置为 false 表示正向计时。
  • iOS 16.2+ 的系统会自动刷这个区间的持续时长,无需手动 update()

4.2 暂停与继续时如何处理

  • 如果计时暂停,就相当于 timerInterval 不再变化,或者你可以把开始和结束的区间都改成同一个时间点,阻止它的走动,从而在 UI 上保持暂停状态;或者直接切换到一个“暂停专用”视图。
  • 继续时,再把 startDate 调整为 (当前时间 - 已经过去的秒数) 这样就能继续往后累计。
  • 由于你依旧保留“内部计时”,所以只要每次暂停/继续时先更新内部数据,再在 ActivityAttributes.ContentState 里塞入合适的 startDate,就可以让系统层的动态文本继续滚动或静止。

4.3 兼容 iOS 16.1 的问题

  • 16.1 没有 Text(timerInterval:),需要用 16.1 版本的 DynamicIsland 里手动更新 elapsedTime。所以在代码里往往需要做 “if #available(iOS 16.2, *) 就用系统级计时;否则自己定期更新 elapsedTime” 这样一个兼容处理。
  • 你可以保留一套“手动更新”逻辑,用于 iOS 16.1 的情况;对 iOS 16.2+ 则可以利用 Text(timerInterval:) 优化,避免多余的 Timer。

5. AppIntent 与内部存储的同步机制

5.1 内部状态和 Live Activity 状态的对应

  • 当用户通过灵动岛或锁屏界面点击“暂停”按钮(触发 PauseTimerIntent)时:

    1. 先更新应用内部状态:将当前计时器状态存储为 .paused,记录当前时间等。
    2. 更新 Live Activity:对 iOS 16.2+,你可以把 startDate...startDate(或者干脆把 endDate = startDate),让 Text(timerInterval:) 不再前进;对 iOS 16.1 则手动改 ContentState.elapsedTime
    3. 如果你还要在暂停之后显示某些交互(例如显示暂停字样),也在 ContentState 中反映。
  • 当用户点击“继续”(ResumeTimerIntent)时:

    1. 恢复内部状态到 .running,并且把 startDate 调整为 “现在 - 已累积时长”。
    2. 更新 Live Activity,把新的开始时间设置进去。
  • 当用户点击“停止”(StopTimerIntent)时:

    1. 设置内部状态为 .stopped,持久化到本地数据库或 UserDefaults。
    2. 结束 Live Activity (activity.end(dismissalPolicy:...))。
    3. 做一些收尾操作,比如停止后台 Shield 或恢复别的监控。

5.2 保证触发 AppIntent 时可以访问并修改 UserDefaults

  • 如果你的 App 已经被杀死,只要尚在内存的 Extension 还在,AppIntent 有机会拉起进程或执行某些操作时,你要确保能够正确地反序列化原先保存在 UserDefaults 中的状态。
  • 如果你有些数据存放在 App Group 里,那么一定要在主 App、Widget Extension、AppIntent Extension 间使用相同的 App Group,保证数据共享。

6. 从“每秒刷新”到“系统自动刷新”的逐步替换

  1. 原先的做法Timer.scheduledTimer + updateActivity()):

    • 手动每秒去计算当前累计时长,然后调用 activity.update(...)
    • 在后台或锁屏时可能受到系统的限制,更新频率会变慢或者被暂停。
  2. 现在的做法(iOS 16.2+,Text(timerInterval:)):

    • 把开始时间和结束时间放入 ContentState(或直接以 ActivityAttributes),由系统去做计时显示,不需要手动的 Timer。
    • 每次只是“状态改变(暂停/继续)”或“重要数据(开始时间)”更新时调用一次 activity.update(...)。而不是每秒都更新。
  3. 保留 fallback

    • iOS 16.1 继续用你原先的 “手动更新” 方法,否则无法使用 Text(timerInterval:)
    • 将这个兼容逻辑和 #available 条件语句配合即可。

7. 具体落地的拆解步骤

  1. 拓展 OfflineTimerAttributes.ContentState

    • 增加 startDate,必要时还可以加 endDate(如果你有预计结束时间的需求)。
    • 保留一个 timerState(running / paused / stopped)以表示当前状态。
    • 对于 iOS 16.2+,在 bodyLockScreenView/DynamicIslandExpandedRegionView 中使用:
      swift
      if context.state.timerState == "running" { Text(timerInterval: context.state.startDate...Date(), countsDown: false) } else if context.state.timerState == "paused" { Text("已暂停") }
      等类似写法(仅举例),实现正向计时或暂停不动。
  2. 在“启动计时”时

    • 内部逻辑:记住 startTime = Date() 并保存到 UserDefaults。
    • Live Activity:创建并启动 Activity<OfflineTimerAttributes>,给 ContentState 初始化 startDate = Date()timerState = "running"
    • 注意区分:iOS 16.2+ 用 timerInterval,iOS 16.1 用手动数值刷新。
  3. 在“暂停计时”时

    • 内部逻辑:根据当前时间计算出已过去多少秒,累加进 accumulatedSec,并置状态为 paused
    • Live Activity:iOS 16.2+ 情况下,可以把新的 ContentState 中的 timerState = "paused",UI 上就切到暂停状态,也可以把 startDate 改成一个固定值让 timerInterval 不再滚动。
    • 兼容 iOS 16.1 情况下:你可以仍然使用 elapsedSeconds 或字符串显示。
  4. 在“继续计时”时

    • 计算出当前要设置的新 startDate = Date() - accumulatedSec,这样 timerInterval 就能从之前的累积时间“继续”往后走。
    • Live Activity:更新 timerState = "running" + startDate = 现在 - 累计秒数
  5. 在“停止计时”时

    • 内部逻辑:把最终累积时长写到数据库或 UserDefaults 并重置。
    • Live Activity:调用 activity.end(...) 结束活跃状态,并设定“收尾时要展示的内容”(如果需要)。
  6. 处理 AppIntent

    • PauseTimerIntent, ResumeTimerIntent, StopTimerIntent 三个 Intent 中,先做内部状态更新(以 UserDefaults 为主),再做Live Activity 的更新/结束操作。
    • 要确保如果 App 已被杀死,也能在下次启动时读取到最新状态;同样地如果在后台,通过 AppIntent 来改变状态时,必须能读/写相同的存储。

8. 核心要点回顾

  1. 持续性与准确性
    • 内部逻辑做主导,保存在本地做持久化;
    • Live Activity/UI 只是呈现层,通过 timerInterval 减少频繁更新、自动保证在后台锁屏时也能实时刷新计时。
  2. 暂停和继续
    • 主要是两个关键数据:accumulatedSec(已累计的总时长) 和 startDate(当前这段的开始时刻)。每次暂停都更新累计时长,每次继续都设置新的 startDate
  3. 多系统版本兼容
    • 16.1 无法用 Text(timerInterval:);16.2+ 可以直接使用,替换掉原先的每秒定时器。
    • 要写 if #available(iOS 16.2, *) {...} else {...} 同时保留老版本逻辑。

小结

综上,改造的关键在于把“所有时间计算”放在内部(以免进程退出后丢状态),而将“实时刷新”交给系统级控件(Text(timerInterval:))来做。这样,只要在关键的状态切换时更新一两次 ActivityContent,就可以让系统自动在锁屏或灵动岛上进行流畅的计时动画,而不必手动每秒去更新它。

当你在正式实现时,可以把以上几个步骤融入到你现有的 OfflineTimerAttributesOfflineTimerHelperLiveActivityManager 等结构中,进行相应字段和逻辑的调整即可。这样既能保留你原有的业务逻辑(包括持久化和统计),也可以充分利用 iOS 16.2+ 提供的新特性来减轻手动刷新负担,并在应用被杀或后台时依旧在灵动岛界面进行准确的时间展示。

Diesen Q&A teilen