2026-06-19 19:53:54 +09:00

158 lines
5.0 KiB
Swift

import Foundation
import Observation
import SwiftData
#if os(iOS)
import ActivityKit
#endif
@MainActor
@Observable
final class TaskViewModel {
private(set) var tasks: [TaskItem] = []
private(set) var activeTimerStartDates: [UUID: Date] = [:]
private(set) var elapsedSeconds: [UUID: Int] = [:]
private var context: ModelContext?
private var timerTasks: [UUID: Task<Void, Never>] = [:]
#if os(iOS)
private var liveActivities: [UUID: Activity<TimerActivityAttributes>] = [:]
#endif
func setup(context: ModelContext) {
self.context = context
fetchTasks()
}
func fetchTasks() {
guard let context else { return }
let descriptor = FetchDescriptor<TaskItem>(sortBy: [SortDescriptor(\.name)])
tasks = (try? context.fetch(descriptor)) ?? []
}
func addTask(name: String, icon: String, type: TaskType, category: Category? = nil) {
guard let context else { return }
let item = TaskItem(name: name, icon: icon, type: type, category: category)
context.insert(item)
try? context.save()
fetchTasks()
}
func deleteTask(_ task: TaskItem) {
stopTimer(for: task)
guard let context else { return }
context.delete(task)
try? context.save()
fetchTasks()
}
func addCount(to task: TaskItem) {
guard let context else { return }
let log = TaskLog(date: .now, duration: 0, count: 1)
log.task = task
context.insert(log)
try? context.save()
}
func toggleTimer(for task: TaskItem) {
if activeTimerStartDates[task.id] != nil {
stopTimer(for: task)
} else {
startTimer(for: task)
}
}
func isTimerRunning(for taskID: UUID) -> Bool {
activeTimerStartDates[taskID] != nil
}
func formattedElapsed(for taskID: UUID) -> String {
let s = elapsedSeconds[taskID] ?? 0
let h = s / 3600
let m = (s % 3600) / 60
let sec = s % 60
return h > 0
? String(format: "%d:%02d:%02d", h, m, sec)
: String(format: "%02d:%02d", m, sec)
}
func todayCount(for task: TaskItem) -> Int {
task.logs
.filter { Calendar.current.isDateInToday($0.date) }
.reduce(0) { $0 + $1.count }
}
// MARK: - Timer
private func startTimer(for task: TaskItem) {
let startDate = Date.now
activeTimerStartDates[task.id] = startDate
elapsedSeconds[task.id] = 0
startLiveActivity(for: task, startDate: startDate)
let taskID = task.id
timerTasks[taskID] = Task { [weak self] in
while !Task.isCancelled {
try? await Task.sleep(for: .seconds(1))
guard !Task.isCancelled, let self else { break }
let elapsed = Int(Date.now.timeIntervalSince(startDate))
self.elapsedSeconds[taskID] = elapsed
await self.updateLiveActivity(taskID: taskID, elapsed: elapsed, startDate: startDate)
}
}
}
private func stopTimer(for task: TaskItem) {
guard let startDate = activeTimerStartDates[task.id] else { return }
timerTasks[task.id]?.cancel()
timerTasks.removeValue(forKey: task.id)
let duration = Date.now.timeIntervalSince(startDate)
activeTimerStartDates.removeValue(forKey: task.id)
elapsedSeconds.removeValue(forKey: task.id)
endLiveActivity(for: task)
guard let context else { return }
let log = TaskLog(date: .now, duration: duration, count: 0)
log.task = task
context.insert(log)
try? context.save()
}
// MARK: - ActivityKit
private func startLiveActivity(for task: TaskItem, startDate: Date) {
#if os(iOS)
guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
let attrs = TimerActivityAttributes(taskName: task.name, taskIcon: task.icon)
let state = TimerActivityAttributes.ContentState(startDate: startDate, elapsedSeconds: 0)
let content = ActivityContent(state: state, staleDate: nil)
do {
let activity = try Activity.request(attributes: attrs, content: content)
liveActivities[task.id] = activity
} catch {
// Live Activity unavailable (simulator, denied, or OS < 16.2)
}
#endif
}
private func updateLiveActivity(taskID: UUID, elapsed: Int, startDate: Date) async {
#if os(iOS)
guard let activity = liveActivities[taskID] else { return }
let state = TimerActivityAttributes.ContentState(startDate: startDate, elapsedSeconds: elapsed)
await activity.update(ActivityContent(state: state, staleDate: nil))
#endif
}
private func endLiveActivity(for task: TaskItem) {
#if os(iOS)
guard let activity = liveActivities[task.id] else { return }
liveActivities.removeValue(forKey: task.id)
Task {
await activity.end(nil, dismissalPolicy: .immediate)
}
#endif
}
}