From 34992906ecaa384fd0fc9e53e024bc2f09570269 Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 6 Feb 2023 15:11:50 +0100 Subject: [PATCH] Improve live timers end state --- LeCountdown/Conductor.swift | 51 ++++-- LeCountdown/Model/LiveTimer.swift | 4 + LeCountdown/TimerRouter.swift | 6 +- LeCountdown/Views/LiveTimerListView.swift | 193 +++++++++++++++++----- 4 files changed, 199 insertions(+), 55 deletions(-) diff --git a/LeCountdown/Conductor.swift b/LeCountdown/Conductor.swift index 41a8c20..d1614bd 100644 --- a/LeCountdown/Conductor.swift +++ b/LeCountdown/Conductor.swift @@ -9,12 +9,12 @@ import Foundation import ActivityKit import BackgroundTasks -enum Key : String { +enum Key: String { case countdowns case stopwatches } -class Conductor : ObservableObject { +class Conductor: ObservableObject { static let maestro: Conductor = Conductor() @@ -23,7 +23,7 @@ class Conductor : ObservableObject { @UserDefault(Key.countdowns.rawValue, defaultValue: [:]) static var savedCountdowns: [String : DateInterval] @UserDefault(Key.stopwatches.rawValue, defaultValue: [:]) static var savedStopwatches: [String : Date] - @Published var liveTimers: [LiveTimer] = [] + @Published private (set) var liveTimers: [LiveTimer] = [] init() { self.currentCountdowns = Conductor.savedCountdowns @@ -43,29 +43,54 @@ class Conductor : ObservableObject { self._buildLiveTimers() } } + + fileprivate var _cleanupTimers: [String : Timer] = [:] + + func removeLiveTimer(id: String) { + self.liveTimers.removeAll(where: { $0.id == id }) + } fileprivate func _buildLiveTimers() { - var countdowns = self.currentCountdowns.map { + let liveCountdowns = self.currentCountdowns.map { return LiveTimer(id: $0, date: $1.end) } - let stopwatches = self.currentStopwatches.map { - return LiveTimer(id: $0, date: $1) + + // add countdown if not present + for liveCountdown in liveCountdowns { + if self.liveTimers.first(where: { $0.id == liveCountdown.id }) == nil { + self.liveTimers.append(liveCountdown) + } } - countdowns.append(contentsOf: stopwatches) - self.liveTimers = countdowns.sorted() - } - - func refreshHack(_ liveTimer: LiveTimer) { + // remove after +// for liveTimer in self.liveTimers { +// let id: String = liveTimer.id +// if liveCountdowns.first(where: { $0.id == id }) == nil { +// let timer = Timer.scheduledTimer(withTimeInterval: 8.0, repeats: false) { _ in +// self.removeLiveTimer(id: id) +// } +// self._cleanupTimers[id] = timer +// } +// } + + let liveStopwatches = self.currentStopwatches.map { + return LiveTimer(id: $0, date: $1) + } + for liveStopwatch in liveStopwatches { + if self.liveTimers.first(where: { $0.id == liveStopwatch.id }) == nil { + self.liveTimers.append(liveStopwatch) + } + } } - + func startCountdown(_ date: Date, countdown: Countdown) { DispatchQueue.main.async { let dateInterval = DateInterval(start: Date(), end: date) self.currentCountdowns[countdown.stringId] = dateInterval self._launchLiveActivity(countdown: countdown, endDate: date) + self._cleanupTimers.removeValue(forKey: countdown.stringId) } } @@ -88,6 +113,8 @@ class Conductor : ObservableObject { if self.currentCountdowns.removeValue(forKey: countdownId) != nil { self._endLiveActivity(countdownId: countdownId) } + + self.removeLiveTimer(id: countdownId) } } diff --git a/LeCountdown/Model/LiveTimer.swift b/LeCountdown/Model/LiveTimer.swift index 552c201..99bad8b 100644 --- a/LeCountdown/Model/LiveTimer.swift +++ b/LeCountdown/Model/LiveTimer.swift @@ -19,4 +19,8 @@ struct LiveTimer: Identifiable, Comparable { func timer(context: NSManagedObjectContext) -> AbstractTimer? { return context.object(stringId: self.id) as? AbstractTimer } + + var ended: Bool { + return self.date < Date() + } } diff --git a/LeCountdown/TimerRouter.swift b/LeCountdown/TimerRouter.swift index 722b887..536548a 100644 --- a/LeCountdown/TimerRouter.swift +++ b/LeCountdown/TimerRouter.swift @@ -29,7 +29,7 @@ class TimerRouter { case let countdown as Countdown: Conductor.maestro.cancelCountdown(id: countdown.stringId) case let stopwatch as Stopwatch: - self._stopStopwatch(stopwatch) + self.stopStopwatch(stopwatch) default: print("missing launcher for \(self)") } @@ -65,11 +65,11 @@ class TimerRouter { } - fileprivate static func _stopStopwatch(_ stopwatch: Stopwatch) { + static func stopStopwatch(_ stopwatch: Stopwatch, end: Date? = nil) { if let start = Conductor.maestro.currentStopwatches[stopwatch.stringId] { Conductor.maestro.currentStopwatches.removeValue(forKey: stopwatch.stringId) do { - try CoreDataRequests.recordActivity(timer: stopwatch, dateInterval: DateInterval(start: start, end: Date())) + try CoreDataRequests.recordActivity(timer: stopwatch, dateInterval: DateInterval(start: start, end: end ?? Date())) } catch { print("could not record") } diff --git a/LeCountdown/Views/LiveTimerListView.swift b/LeCountdown/Views/LiveTimerListView.swift index b049cd1..82a3430 100644 --- a/LeCountdown/Views/LiveTimerListView.swift +++ b/LeCountdown/Views/LiveTimerListView.swift @@ -7,56 +7,160 @@ import SwiftUI -struct LiveTimerView: View { +struct GreenCheckmarkView: View { + var body: some View { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + .font(.title) + .frame(minWidth: 0.0, maxWidth: 60.0, minHeight: 0.0, maxHeight: .infinity) + } +} + +class LiveStopwatchModel: ObservableObject { + + @Published var endDate: Date? = nil + + func stop(_ stopwatch: Stopwatch) { + + let now = Date() + self.endDate = now + + TimerRouter.stopStopwatch(stopwatch, end: now) + } +} + +struct LiveStopwatchView: View { @Environment(\.managedObjectContext) private var viewContext @EnvironmentObject var conductor: Conductor - @State var timer: AbstractTimer + @StateObject var model: LiveStopwatchModel = LiveStopwatchModel() + + @State var stopwatch: Stopwatch var date: Date var body: some View { + + let running = (self.model.endDate == nil) + HStack { - Text(timer.displayName.uppercased()).padding() - + + Text(stopwatch.displayName.uppercased()).padding() + Spacer() - TimelineView(.periodic(from: self.date, by: 0.01)) { context in - Text(self._formattedDuration(date: context.date)) + + if running { + TimelineView(.periodic(from: self.date, by: 0.01)) { context in + Text(self._formattedDuration(date: context.date)) + .font(.title2) + .padding(.trailing) + .minimumScaleFactor(0.1) + } + } else { + let duration = self.model.endDate?.timeIntervalSince(self.date) ?? 0.0 + Text(duration.hourMinuteSecondHS) .font(.title2) .padding(.trailing) .minimumScaleFactor(0.1) } - Button { - self._stopTimer(timer) - } label: { - Image(systemName: "xmark.circle.fill") - .font(.title) - .foregroundColor(.white) - .cornerRadius(8.0) - .frame(minWidth: 0.0, maxWidth: 60.0, minHeight: 0.0, maxHeight: .infinity) - }.background(.red) + if running { + + Button { + self.model.stop(stopwatch) + } label: { + + Image(systemName: "stop.circle.fill") + .font(.title) + .foregroundColor(.white) + .cornerRadius(8.0) + .frame(minWidth: 0.0, maxWidth: 60.0, minHeight: 0.0, maxHeight: .infinity) + }.background(.red) + + } else { + GreenCheckmarkView() + } + }.onTapGesture { + withAnimation { + self._dismiss() + } } + .frame(height: 55.0) + .foregroundColor(.white) + .monospaced() + .background(Color(white: 0.2)) + .cornerRadius(16.0) + } + + fileprivate func _dismiss() { + conductor.removeLiveTimer(id: self.stopwatch.stringId) } fileprivate func _formattedDuration(date: Date) -> String { - if self.timer is Stopwatch { - let duration = date.timeIntervalSince(self.date) - return duration.hourMinuteSecondHS - } else { // countdown - let duration = self.date.timeIntervalSince(date) - return duration.minuteSecond - } + let duration = date.timeIntervalSince(self.date) + return duration.hourMinuteSecondHS } +} + +struct LiveCountdownView: View { - fileprivate func _stopTimer(_ timer: AbstractTimer?) { - - guard let timer else { - return + @Environment(\.managedObjectContext) private var viewContext + @EnvironmentObject var conductor: Conductor + + @State var countdown: Countdown + var date: Date + + var body: some View { + HStack { + Text(self.countdown.displayName.uppercased()).padding() + Spacer() + TimelineView(.periodic(from: self.date, by: 0.01)) { context in + + if self.date > context.date { + + HStack { + Text(self._formattedDuration(date: context.date)) + .font(.title2) + .minimumScaleFactor(0.1) + .padding(.trailing) + Button { + self._cancelCountdown() + } label: { + Image(systemName: "xmark.circle.fill") + .font(.title) + .foregroundColor(.white) + .cornerRadius(8.0) + .frame(minWidth: 0.0, maxWidth: 60.0, minHeight: 0.0, maxHeight: .infinity) + }.background(.red) + + } + } else { + GreenCheckmarkView() + } + } + }.onTapGesture { + withAnimation { + self._dismiss() + } } - - TimerRouter.stopTimer(timer: timer) - + .frame(height: 55.0) + .foregroundColor(.white) + .monospaced() + .background(Color(white: 0.2)) + .cornerRadius(16.0) + } + + fileprivate func _dismiss() { + conductor.removeLiveTimer(id: self.countdown.stringId) + } + + fileprivate func _formattedDuration(date: Date) -> String { + let duration = self.date.timeIntervalSince(date) + return duration.minuteSecond + } + + fileprivate func _cancelCountdown() { + Conductor.maestro.cancelCountdown(id: self.countdown.stringId) } } @@ -72,12 +176,15 @@ struct LiveTimerListView: View { if let timer: AbstractTimer = liveTimer.timer(context: self.viewContext) { - LiveTimerView(timer: timer, date: liveTimer.date) - .frame(height: 55.0) - .foregroundColor(.white) - .monospaced() - .background(Color(white: 0.2)) - .cornerRadius(16.0) + switch timer { + case let cd as Countdown: + LiveCountdownView(countdown: cd, date: liveTimer.date) + case let sw as Stopwatch: + LiveStopwatchView(stopwatch: sw, date: liveTimer.date) + default: + Text("unmanaged timer: \(timer)") + } + } } @@ -90,11 +197,17 @@ struct LiveTimerListView: View { struct LiveTimerView_Previews: PreviewProvider { - init() { - Conductor.maestro.currentCountdowns["fef"] = DateInterval(start: Date(), end: Date()) - } - static var previews: some View { - LiveTimerListView().environmentObject(Conductor.maestro) +// LiveTimerListView().environmentObject(Conductor.maestro) + + Group { + VStack(spacing: 20.0) { + LiveCountdownView(countdown: Countdown.fake(context: PersistenceController.preview.container.viewContext), date: Date().addingTimeInterval(3600.0)) + .environmentObject(Conductor.maestro) + LiveStopwatchView(stopwatch: Stopwatch.fake(context: PersistenceController.preview.container.viewContext), date: Date()) + .environmentObject(Conductor.maestro) + } + }.padding() } + }