Adds live view

release
Laurent 3 years ago
parent 6137196be7
commit 7797796901
  1. 8
      LeCountdown.xcodeproj/project.pbxproj
  2. 47
      LeCountdown/Conductor.swift
  3. 22
      LeCountdown/Model/LiveTimer.swift
  4. 27
      LeCountdown/TimerRouter.swift
  5. 17
      LeCountdown/Utils/ViewModifiers.swift
  6. 15
      LeCountdown/Views/Alarm/AlarmDialView.swift
  7. 25
      LeCountdown/Views/ContentView.swift
  8. 2
      LeCountdown/Views/Countdown/CountdownDialView.swift
  9. 70
      LeCountdown/Views/LiveTimerView.swift
  10. 2
      LeCountdown/Views/Stopwatch/StopwatchDialView.swift

@ -62,6 +62,8 @@
C4742B59298411E800D5D950 /* CountdownFormView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4742B58298411E800D5D950 /* CountdownFormView.swift */; };
C4742B5B298414B000D5D950 /* ImageSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4742B5A298414B000D5D950 /* ImageSelectionView.swift */; };
C4742B5F2984205000D5D950 /* ViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4742B5E2984205000D5D950 /* ViewModifiers.swift */; };
C498E59F298D4DEA00E90DE0 /* LiveTimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C498E59E298D4DEA00E90DE0 /* LiveTimerView.swift */; };
C498E5A1298D543900E90DE0 /* LiveTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C498E5A0298D543900E90DE0 /* LiveTimer.swift */; };
C4F8B1532987FE6F005C86A5 /* LaunchWidgetLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7D72981216200BF3EF9 /* LaunchWidgetLiveActivity.swift */; };
C4F8B15729891271005C86A5 /* Conductor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B15629891271005C86A5 /* Conductor.swift */; };
C4F8B15929891528005C86A5 /* forest_stream.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = C4F8B15829891528005C86A5 /* forest_stream.mp3 */; };
@ -220,6 +222,8 @@
C4742B58298411E800D5D950 /* CountdownFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountdownFormView.swift; sourceTree = "<group>"; };
C4742B5A298414B000D5D950 /* ImageSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSelectionView.swift; sourceTree = "<group>"; };
C4742B5E2984205000D5D950 /* ViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifiers.swift; sourceTree = "<group>"; };
C498E59E298D4DEA00E90DE0 /* LiveTimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTimerView.swift; sourceTree = "<group>"; };
C498E5A0298D543900E90DE0 /* LiveTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTimer.swift; sourceTree = "<group>"; };
C4F8B15629891271005C86A5 /* Conductor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conductor.swift; sourceTree = "<group>"; };
C4F8B15829891528005C86A5 /* forest_stream.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = forest_stream.mp3; sourceTree = "<group>"; };
C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableForEach.swift; sourceTree = "<group>"; };
@ -413,6 +417,7 @@
C4F8B188298AC248005C86A5 /* Generation */,
C4060DCA297AE73D003FAB80 /* LeCountdown.xcdatamodeld */,
C438C80C2982847300BF3EF9 /* CoreDataRequests.swift */,
C498E5A0298D543900E90DE0 /* LiveTimer.swift */,
C438C806298195E600BF3EF9 /* Model+Extensions.swift */,
C438C7C4298024E900BF3EF9 /* NSManagedContext+Extensions.swift */,
C4060DC8297AE73D003FAB80 /* Persistence.swift */,
@ -440,6 +445,7 @@
C4F8B1D3298BF686005C86A5 /* Components */,
C4060DC1297AE73B003FAB80 /* ContentView.swift */,
C4F8B1CF298BF2E2005C86A5 /* DialView.swift */,
C498E59E298D4DEA00E90DE0 /* LiveTimerView.swift */,
C438C80E29828B8600BF3EF9 /* RecordsView.swift */,
C4742B5A298414B000D5D950 /* ImageSelectionView.swift */,
C4F8B165298A9ABB005C86A5 /* SoundImageFormView.swift */,
@ -720,6 +726,7 @@
files = (
C4F8B1D2298BF646005C86A5 /* PermissionAlertView.swift in Sources */,
C4060DC9297AE73D003FAB80 /* Persistence.swift in Sources */,
C498E5A1298D543900E90DE0 /* LiveTimer.swift in Sources */,
C4F8B1AB298AC3A0005C86A5 /* Countdown+CoreDataProperties.swift in Sources */,
C438C80F29828B8600BF3EF9 /* RecordsView.swift in Sources */,
C4F8B187298AC234005C86A5 /* Activity+CoreDataProperties.swift in Sources */,
@ -764,6 +771,7 @@
C438C7C5298024E900BF3EF9 /* NSManagedContext+Extensions.swift in Sources */,
C4F8B15F298961A7005C86A5 /* ReorderableForEach.swift in Sources */,
C4F8B1AC298AC3A0005C86A5 /* Alarm+CoreDataProperties.swift in Sources */,
C498E59F298D4DEA00E90DE0 /* LiveTimerView.swift in Sources */,
C4060DC0297AE73B003FAB80 /* LeCountdownApp.swift in Sources */,
C438C7E02981216300BF3EF9 /* LaunchWidget.intentdefinition in Sources */,
C4742B5729840F6400D5D950 /* CoolPic.swift in Sources */,

@ -15,35 +15,50 @@ class Conductor : ObservableObject {
@Published var soundPlayer: SoundPlayer? = nil
@UserDefault(Key.dates.rawValue, defaultValue: [:]) static var savedDates: [String : DateInterval]
@UserDefault(Key.stopwatch.rawValue, defaultValue: [:]) static var savedStopwatches: [String : Date]
@UserDefault(Key.countdowns.rawValue, defaultValue: [:]) static var savedCountdowns: [String : DateInterval]
@UserDefault(Key.stopwatches.rawValue, defaultValue: [:]) static var savedStopwatches: [String : Date]
@Published var liveTimers: [LiveTimer] = []
init() {
self.timerDates = Conductor.savedDates
self.stopwatchIntervals = Conductor.savedStopwatches
self.currentCountdowns = Conductor.savedCountdowns
self.currentStopwatches = Conductor.savedStopwatches
}
@Published var timerDates: [String : DateInterval] = [:] {
@Published var currentCountdowns: [String : DateInterval] = [:] {
didSet {
Conductor.savedDates = timerDates
Conductor.savedCountdowns = currentCountdowns
self._buildLiveTimers()
}
}
@Published var stopwatchIntervals: [String : Date] = [:] {
@Published var currentStopwatches: [String : Date] = [:] {
didSet {
Conductor.savedStopwatches = stopwatchIntervals
Conductor.savedStopwatches = currentStopwatches
self._buildLiveTimers()
}
}
fileprivate func _buildLiveTimers() {
var countdowns = self.currentCountdowns.map {
return LiveTimer(id: $0, date: $1.end)
}
var stopwatches = self.currentStopwatches.map {
return LiveTimer(id: $0, date: $1)
}
countdowns.append(contentsOf: stopwatches)
self.liveTimers = countdowns.sorted()
}
enum Key : String {
case dates
case stopwatch
case countdowns
case stopwatches
}
func startCountdown(_ date: Date, countdown: Countdown) {
DispatchQueue.main.async {
let dateInterval = DateInterval(start: Date(), end: date)
self.timerDates[countdown.stringId] = dateInterval
self.currentCountdowns[countdown.stringId] = dateInterval
self._launchLiveActivity(countdown: countdown, endDate: date)
}
@ -60,15 +75,17 @@ class Conductor : ObservableObject {
self._recordActivity(countdownId: countdownId)
}
if self.timerDates.removeValue(forKey: countdownId) != nil {
if self.currentCountdowns.removeValue(forKey: countdownId) != nil {
self._endLiveActivity(countdownId: countdownId)
}
self.stopSoundIfPossible() // multi use
}
}
func cleanup() {
let now = Date()
for (key, value) in self.timerDates {
for (key, value) in self.currentCountdowns {
if value.end < now {
self.endCountdown(countdownId: key, cancel: false)
}
@ -78,7 +95,7 @@ class Conductor : ObservableObject {
fileprivate func _recordActivity(countdownId: String) {
let context = PersistenceController.shared.container.viewContext
if let countdown = context.object(stringId: countdownId) as? Countdown,
let dateInterval = self.timerDates[countdownId] {
let dateInterval = self.currentCountdowns[countdownId] {
do {
try CoreDataRequests.recordActivity(timer: countdown, dateInterval: dateInterval)
} catch {
@ -154,7 +171,7 @@ class Conductor : ObservableObject {
func updateLiveActivities() {
print("update live activity...")
for (countdownId, interval) in self.timerDates {
for (countdownId, interval) in self.currentCountdowns {
if interval.end < Date() {
self._endLiveActivity(countdownId: countdownId)

@ -0,0 +1,22 @@
//
// LiveData.swift
// LeCountdown
//
// Created by Laurent Morvillier on 03/02/2023.
//
import Foundation
import CoreData
struct LiveTimer: Identifiable, Comparable {
var id: String
var date: Date
static func < (lhs: LiveTimer, rhs: LiveTimer) -> Bool {
return lhs.date < rhs.date
}
func timer(context: NSManagedObjectContext) -> AbstractTimer? {
return context.object(stringId: self.id) as? AbstractTimer
}
}

@ -24,6 +24,17 @@ class TimerRouter {
}
static func stopTimer(timer: AbstractTimer) {
switch timer {
case let countdown as Countdown:
Conductor.maestro.endCountdown(countdownId: countdown.stringId, cancel: true)
case let stopwatch as Stopwatch:
self._stopStopwatch(stopwatch)
default:
print("missing launcher for \(self)")
}
}
fileprivate static func _launchCountdown(_ countdown: Countdown, handler: @escaping (Result<Bool, Error>) -> Void) {
UNUserNotificationCenter.current().getNotificationSettings { settings in
@ -50,19 +61,19 @@ class TimerRouter {
fileprivate static func _startStopwatch(_ stopwatch: Stopwatch, handler: @escaping (Result<Bool, Error>) -> Void) {
if let start = Conductor.maestro.stopwatchIntervals[stopwatch.stringId] {
Conductor.maestro.stopwatchIntervals.removeValue(forKey: stopwatch.stringId)
Conductor.maestro.currentStopwatches[stopwatch.stringId] = Date()
}
fileprivate static func _stopStopwatch(_ stopwatch: Stopwatch) {
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()))
} catch {
handler(.failure(error))
print("could not record")
}
} else {
Conductor.maestro.stopwatchIntervals[stopwatch.stringId] = Date()
}
}
}

@ -13,7 +13,11 @@ extension View {
func roundedCorner(selected: Bool) -> some View {
modifier(RoundedCornerSelection(selected: selected))
}
func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View {
clipShape( RoundedCorner(radius: radius, corners: corners) )
}
}
struct RoundedCornerSelection: ViewModifier {
@ -33,3 +37,14 @@ struct RoundedCornerSelection: ViewModifier {
}
}
struct RoundedCorner: Shape {
var radius: CGFloat = .infinity
var corners: UIRectCorner = .allCorners
func path(in rect: CGRect) -> Path {
let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
return Path(path.cgPath)
}
}

@ -12,20 +12,16 @@ struct AlarmDialView: View {
@ObservedObject var alarm: Alarm
var body: some View {
VStack {
HStack {
HStack {
VStack {
Text(alarm.activity?.name?.uppercased() ?? "")
Spacer()
}
if let fireDate = alarm.fireDate {
HStack {
if let fireDate = alarm.fireDate {
Text(fireDate, style: .time)
Spacer()
}
Spacer()
}
Spacer()
}
}
@ -35,5 +31,6 @@ struct AlarmDialView: View {
struct AlarmDialView_Previews: PreviewProvider {
static var previews: some View {
AlarmDialView(alarm: Alarm.fake(context: PersistenceController.preview.container.viewContext))
.background(.cyan)
}
}

@ -46,7 +46,7 @@ struct ContentView<T : AbstractTimer>: View {
GeometryReader { reader in
let width: CGFloat = reader.size.width / 2 - 10.0
ZStack(alignment: .bottom) {
VStack {
ScrollView {
@ -65,24 +65,13 @@ struct ContentView<T : AbstractTimer>: View {
self._reorder(from: from, to: to)
}
}
}
}.padding(.horizontal, itemSpacing)
if Conductor.maestro.soundPlayer != nil {
Button {
Conductor.maestro.stopSoundIfPossible()
} label: {
Text("STOP")
.frame(minWidth: 0.0, maxWidth: .infinity, minHeight: 75.0, maxHeight: 75.0)
.foregroundColor(.white)
.background(.red)
.cornerRadius(16.0)
}.padding()
}
LiveTimerView() .environment(\.managedObjectContext, viewContext)
.background(Color(white: 0.9))
.cornerRadius(16.0, corners: [.topRight, .topLeft])
}
}.padding(.horizontal, itemSpacing)
}
.navigationTitle("\(String(describing: T.self))")
.alert(boringContext.error?.localizedDescription ?? "missing error", isPresented: $boringContext.showDefaultAlert) {
Button("OK", role: .cancel) { }
@ -231,7 +220,7 @@ struct MainToolbarView: View {
fileprivate extension Countdown {
var endDate: Date? {
return Conductor.maestro.timerDates[self.stringId]?.end
return Conductor.maestro.currentCountdowns[self.stringId]?.end
}
var isLive: Bool {

@ -19,7 +19,7 @@ struct CountdownDialView: View {
VStack(alignment: .leading) {
Text(countdown.activity?.name?.uppercased() ?? "")
// let dateInterval = DateInterval(start: Date(), end: Date())
if let dateInterval = conductor.timerDates[countdown.stringId] {
if let dateInterval = conductor.currentCountdowns[countdown.stringId] {
Text(dateInterval.end, style: .timer)
Spacer()
HStack {

@ -0,0 +1,70 @@
//
// LiveTimerView.swift
// LeCountdown
//
// Created by Laurent Morvillier on 03/02/2023.
//
import SwiftUI
struct LiveTimerView: View {
@Environment(\.managedObjectContext) private var viewContext
@EnvironmentObject var conductor: Conductor
var body: some View {
LazyVStack {
ForEach(conductor.liveTimers) { liveTimer in
let timer = liveTimer.timer(context: self.viewContext)
HStack {
Text(timer?.displayName.uppercased() ?? "missing")
Spacer()
Text(liveTimer.date, style: .timer)
Spacer()
Button {
self._stopTimer(timer)
} label: {
Text("STOP")
.padding(8.0)
.foregroundColor(.red)
.background(.white)
.fontWeight(.semibold)
.cornerRadius(8.0)
}//.buttonStyle(.bordered).tint(.red)
}
.padding()
.frame(height: 55.0)
.foregroundColor(.white)
.monospaced()
.background(.cyan)
.cornerRadius(16.0)
}
}.padding(8.0)
}
fileprivate func _stopTimer(_ timer: AbstractTimer?) {
guard let timer else {
return
}
TimerRouter.stopTimer(timer: timer)
}
}
struct LiveTimerView_Previews: PreviewProvider {
init() {
Conductor.maestro.currentCountdowns["fef"] = DateInterval(start: Date(), end: Date())
}
static var previews: some View {
LiveTimerView().environmentObject(Conductor.maestro)
}
}

@ -18,7 +18,7 @@ struct StopwatchDialView: View {
VStack(alignment: .leading) {
Text(stopwatch.activity?.name?.uppercased() ?? "")
if let start = conductor.stopwatchIntervals[stopwatch.stringId] {
if let start = conductor.currentStopwatches[stopwatch.stringId] {
Text(start, style: .timer)
}

Loading…
Cancel
Save