Improve live timers end state

release
Laurent 3 years ago
parent f85fe20df7
commit 34992906ec
  1. 51
      LeCountdown/Conductor.swift
  2. 4
      LeCountdown/Model/LiveTimer.swift
  3. 6
      LeCountdown/TimerRouter.swift
  4. 193
      LeCountdown/Views/LiveTimerListView.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)
}
}

@ -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()
}
}

@ -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")
}

@ -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()
}
}

Loading…
Cancel
Save