From b9fde1b0c24049a83d6d050e423210e915b9a77a Mon Sep 17 00:00:00 2001 From: Laurent Date: Mon, 6 Feb 2023 17:54:20 +0100 Subject: [PATCH] Improvement and fixes --- LaunchWidget/LaunchWidgetLiveActivity.swift | 29 +++-- LeCountdown.xcodeproj/project.pbxproj | 6 + LeCountdown/AppDelegate.swift | 2 +- LeCountdown/Conductor.swift | 116 +++++++++++------- LeCountdown/LeCountdownApp.swift | 2 +- LeCountdown/Model/Model+Extensions.swift | 8 +- LeCountdown/Sound/Sound.swift | 2 +- LeCountdown/Sound/SoundPlayer.swift | 16 ++- LeCountdown/TimerRouter.swift | 17 +-- .../Views/Components/GreenCheckmarkView.swift | 23 ++++ LeCountdown/Views/ContentView.swift | 6 - LeCountdown/Views/LiveTimerListView.swift | 12 +- 12 files changed, 150 insertions(+), 89 deletions(-) create mode 100644 LeCountdown/Views/Components/GreenCheckmarkView.swift diff --git a/LaunchWidget/LaunchWidgetLiveActivity.swift b/LaunchWidget/LaunchWidgetLiveActivity.swift index ae6b3f2..46d62cb 100644 --- a/LaunchWidget/LaunchWidgetLiveActivity.swift +++ b/LaunchWidget/LaunchWidgetLiveActivity.swift @@ -18,7 +18,7 @@ struct LaunchWidgetAttributes: ActivityAttributes { // Fixed non-changing properties about your activity go here! var id: String var name: String - var endDate: Date + var date: Date } @@ -45,13 +45,20 @@ struct LaunchWidgetLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: LaunchWidgetAttributes.self) { context in - let range = Date()...context.attributes.endDate +// let range = Date()...context.attributes.date // Lock screen/banner UI goes here HStack { - Text(context.attributes.name) + Text(context.attributes.name.uppercased()) Spacer() - Text(timerInterval: range, pauseTime: range.lowerBound) + Text(context.attributes.date, style: .timer) + .font(.title) + +// if Date() < context.attributes.date { +// Text(context.attributes.date, style: .timer) +// } else { +// GreenCheckmarkView() +// } // if context.attributes.endDate > self.model.now { // Text(context.attributes.endDate, style: .timer) @@ -60,19 +67,21 @@ struct LaunchWidgetLiveActivity: Widget { // Text("It's time!") // } }.padding() - .font(.title) - .activityBackgroundTint(Color.cyan) - .activitySystemActionForegroundColor(Color.black) + .monospaced() + .foregroundColor(.white) + .activityBackgroundTint(Color(white: 0.2)) + .activitySystemActionForegroundColor(.white) } dynamicIsland: { context in DynamicIsland { // Expanded UI goes here. Compose the expanded UI through // various regions, like leading/trailing/center/bottom DynamicIslandExpandedRegion(.leading) { - Text(context.attributes.name) + Text(context.attributes.name.uppercased()) + .monospaced() } DynamicIslandExpandedRegion(.trailing) { - Text(context.attributes.endDate, style: .timer) + Text(context.attributes.date, style: .timer) .monospaced() } DynamicIslandExpandedRegion(.bottom) { @@ -104,7 +113,7 @@ struct LaunchWidgetLiveActivity_Previews: PreviewProvider { static let attributes = LaunchWidgetAttributes( id: "", name: "Tea", - endDate: Date().addingTimeInterval(3600.0)) + date: Date().addingTimeInterval(3600.0)) static let contentState = LaunchWidgetAttributes.ContentState(ended: false) diff --git a/LeCountdown.xcodeproj/project.pbxproj b/LeCountdown.xcodeproj/project.pbxproj index f81a4d5..c2d9dd1 100644 --- a/LeCountdown.xcodeproj/project.pbxproj +++ b/LeCountdown.xcodeproj/project.pbxproj @@ -65,6 +65,8 @@ C498E59F298D4DEA00E90DE0 /* LiveTimerListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C498E59E298D4DEA00E90DE0 /* LiveTimerListView.swift */; }; C498E5A1298D543900E90DE0 /* LiveTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C498E5A0298D543900E90DE0 /* LiveTimer.swift */; }; C498E5A3298D720600E90DE0 /* TestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C498E5A2298D720600E90DE0 /* TestView.swift */; }; + C498E5A5299152B400E90DE0 /* GreenCheckmarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C498E5A4299152B400E90DE0 /* GreenCheckmarkView.swift */; }; + C498E5A6299152C600E90DE0 /* GreenCheckmarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C498E5A4299152B400E90DE0 /* GreenCheckmarkView.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 */; }; @@ -226,6 +228,7 @@ C498E59E298D4DEA00E90DE0 /* LiveTimerListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTimerListView.swift; sourceTree = ""; }; C498E5A0298D543900E90DE0 /* LiveTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTimer.swift; sourceTree = ""; }; C498E5A2298D720600E90DE0 /* TestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestView.swift; sourceTree = ""; }; + C498E5A4299152B400E90DE0 /* GreenCheckmarkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GreenCheckmarkView.swift; sourceTree = ""; }; C4F8B15629891271005C86A5 /* Conductor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conductor.swift; sourceTree = ""; }; C4F8B15829891528005C86A5 /* forest_stream.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = forest_stream.mp3; sourceTree = ""; }; C4F8B15E298961A7005C86A5 /* ReorderableForEach.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReorderableForEach.swift; sourceTree = ""; }; @@ -530,6 +533,7 @@ isa = PBXGroup; children = ( C4F8B1D1298BF646005C86A5 /* PermissionAlertView.swift */, + C498E5A4299152B400E90DE0 /* GreenCheckmarkView.swift */, ); path = Components; sourceTree = ""; @@ -745,6 +749,7 @@ C438C7FF2981300500BF3EF9 /* IntentDataProvider.swift in Sources */, C4F8B183298AC234005C86A5 /* Stopwatch+CoreDataProperties.swift in Sources */, C4F8B1A8298AC2FC005C86A5 /* AbstractSoundTimer+CoreDataProperties.swift in Sources */, + C498E5A5299152B400E90DE0 /* GreenCheckmarkView.swift in Sources */, C4F8B1B8298AC81D005C86A5 /* CountdownDialView.swift in Sources */, C445FA922987CC8A0054D761 /* Sound.swift in Sources */, C4F8B1D0298BF2E2005C86A5 /* DialView.swift in Sources */, @@ -805,6 +810,7 @@ files = ( C4F8B1AF298AC451005C86A5 /* Countdown+CoreDataProperties.swift in Sources */, C445FA932987CF280054D761 /* Sound.swift in Sources */, + C498E5A6299152C600E90DE0 /* GreenCheckmarkView.swift in Sources */, C438C7EB2981266F00BF3EF9 /* SingleCountdownView.swift in Sources */, C438C7D62981216200BF3EF9 /* LaunchWidgetBundle.swift in Sources */, C4F8B18B298AC288005C86A5 /* Record+CoreDataClass.swift in Sources */, diff --git a/LeCountdown/AppDelegate.swift b/LeCountdown/AppDelegate.swift index 9981a58..ed80bac 100644 --- a/LeCountdown/AppDelegate.swift +++ b/LeCountdown/AppDelegate.swift @@ -13,7 +13,7 @@ class AppDelegate : NSObject, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { UNUserNotificationCenter.current().delegate = self - Conductor.maestro.cleanup() + Conductor.maestro.cleanupCountdowns() return true } diff --git a/LeCountdown/Conductor.swift b/LeCountdown/Conductor.swift index c2446da..2835b2a 100644 --- a/LeCountdown/Conductor.swift +++ b/LeCountdown/Conductor.swift @@ -52,6 +52,7 @@ class Conductor: ObservableObject { fileprivate var _cleanupTimers: [String : Timer] = [:] func removeLiveTimer(id: String) { + self.stopSoundIfPossible() self.liveTimers.removeAll(where: { $0.id == id }) } @@ -89,21 +90,36 @@ class Conductor: ObservableObject { } } + + func notifyUser(countdownId: String) { + self._playSound(timerId: countdownId) + self._endCountdown(countdownId: countdownId, cancel: false) + } + + fileprivate func _recordActivity(countdownId: String) { + let context = PersistenceController.shared.container.viewContext + if let countdown = context.object(stringId: countdownId) as? Countdown, + let dateInterval = self.currentCountdowns[countdownId] { + do { + try CoreDataRequests.recordActivity(timer: countdown, dateInterval: dateInterval) + } catch { + print("Could not record activity = \(error)") + // TODO: show error to user + } + } + } + + // MARK: - Countdown 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._launchLiveActivity(countdown: countdown, endDate: date) self._cleanupTimers.removeValue(forKey: countdown.stringId) } } - - func notifyUser(countdownId: String) { - self._playSound(countdownId: countdownId) - self._endCountdown(countdownId: countdownId, cancel: false) - } func cancelCountdown(id: String) { CountdownScheduler.master.cancelCurrentNotifications(countdownId: id) @@ -115,16 +131,17 @@ class Conductor: ObservableObject { if !cancel { self._recordActivity(countdownId: countdownId) } + self.currentCountdowns.removeValue(forKey: countdownId) - if self.currentCountdowns.removeValue(forKey: countdownId) != nil { - self._endLiveActivity(countdownId: countdownId) - } +// if self.currentCountdowns.removeValue(forKey: countdownId) != nil { +// self._endLiveActivity(countdownId: countdownId) +// } self.removeLiveTimer(id: countdownId) } } - func cleanup() { + func cleanupCountdowns() { let now = Date() for (key, value) in self.currentCountdowns { if value.end < now { @@ -133,37 +150,55 @@ 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.currentCountdowns[countdownId] { + // MARK: - Stopwatch + + func startStopwatch(_ stopwatch: Stopwatch) { + DispatchQueue.main.async { + let now = Date() + Conductor.maestro.currentStopwatches[stopwatch.stringId] = now + self._launchLiveActivity(stopwatch: stopwatch, start: now) + } + } + + 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: countdown, dateInterval: dateInterval) + try CoreDataRequests.recordActivity(timer: stopwatch, dateInterval: DateInterval(start: start, end: end ?? Date())) } catch { - print("Could not record activity = \(error)") - // TODO: show error to user + print("could not record") } + self._endLiveActivity(timerId: stopwatch.stringId) } } // MARK: - Sound - fileprivate func _playSound(countdownId: String) { - - let countdown = PersistenceController.shared.container.viewContext.object(stringId: countdownId) as? Countdown + fileprivate func _playSound(timerId: String) { - let coolSound = countdown?.coolSound ?? Sound.allCases[0] + let context = PersistenceController.shared.container.viewContext - do { - let soundFile = try coolSound.soundFile() - let soundPlayer = SoundPlayer() - self.soundPlayer = soundPlayer - try soundPlayer.playSound(soundFile: soundFile, repeats: countdown?.repeats ?? true) - } catch { - print("error = \(error)") - // TODO: manage error + var coolSound: Sound? = nil + switch context.object(stringId: timerId) { + case let cd as Countdown: + coolSound = cd.coolSound + case let sw as Stopwatch: + coolSound = sw.coolSound + default: + break + } + + if let coolSound { + do { + let soundFile = try coolSound.soundFile() + let soundPlayer = SoundPlayer() + self.soundPlayer = soundPlayer + try soundPlayer.playSound(soundFile: soundFile, repeats: false) + } catch { + print("error = \(error)") + // TODO: manage error + } } - } func stopSoundIfPossible() { @@ -173,13 +208,13 @@ class Conductor: ObservableObject { // MARK: - Live Activity - fileprivate func _launchLiveActivity(countdown: Countdown, endDate: Date) { + fileprivate func _launchLiveActivity(stopwatch: Stopwatch, start: Date) { if ActivityAuthorizationInfo().areActivitiesEnabled { let contentState = LaunchWidgetAttributes.ContentState(ended: false) - let attributes = LaunchWidgetAttributes(id: countdown.stringId, name: countdown.displayName, endDate: endDate) - let activityContent = ActivityContent(state: contentState, staleDate: endDate.addingTimeInterval(30.0)) + let attributes = LaunchWidgetAttributes(id: stopwatch.stringId, name: stopwatch.displayName, date: start) + let activityContent = ActivityContent(state: contentState, staleDate: nil) do { let liveActivity = try ActivityKit.Activity.request(attributes: attributes, content: activityContent) @@ -188,7 +223,7 @@ class Conductor: ObservableObject { print("Error requesting countdown Live Activity \(error.localizedDescription).") } - self._scheduleAppRefresh(countdown: countdown) +// self._scheduleAppRefresh(countdown: countdown) } } @@ -196,7 +231,6 @@ class Conductor: ObservableObject { fileprivate func _scheduleAppRefresh(countdown: Countdown) { let request = BGAppRefreshTaskRequest(identifier: BGTaskIdentifier.refresh.rawValue) request.earliestBeginDate = Date(timeIntervalSinceNow: countdown.duration) - do { try BGTaskScheduler.shared.submit(request) print("request submitted with date: \(String(describing: request.earliestBeginDate))") @@ -205,8 +239,8 @@ class Conductor: ObservableObject { } } - fileprivate func _liveActivity(countdownId: String) -> ActivityKit.Activity? { - return ActivityKit.Activity.activities.first(where: { $0.attributes.id == countdownId } ) + fileprivate func _liveActivity(timerId: String) -> ActivityKit.Activity? { + return ActivityKit.Activity.activities.first(where: { $0.attributes.id == timerId } ) } func updateLiveActivities() { @@ -215,7 +249,7 @@ class Conductor: ObservableObject { for (countdownId, interval) in self.currentCountdowns { if interval.end < Date() { - self._endLiveActivity(countdownId: countdownId) + self._endLiveActivity(timerId: countdownId) } @@ -237,11 +271,11 @@ class Conductor: ObservableObject { } - fileprivate func _endLiveActivity(countdownId: String) { + fileprivate func _endLiveActivity(timerId: String) { - print("Try to end the Live Activity: \(countdownId)") + print("Try to end the Live Activity: \(timerId)") - if let activity = self._liveActivity(countdownId: countdownId) { + if let activity = self._liveActivity(timerId: timerId) { Task { let state = LaunchWidgetAttributes.ContentState(ended: true) let content = ActivityContent(state: state, staleDate: Date()) diff --git a/LeCountdown/LeCountdownApp.swift b/LeCountdown/LeCountdownApp.swift index 9937ce9..76a7071 100644 --- a/LeCountdown/LeCountdownApp.swift +++ b/LeCountdown/LeCountdownApp.swift @@ -64,7 +64,7 @@ struct LeCountdownApp: App { } fileprivate func _willEnterForegroundNotification() { - Conductor.maestro.cleanup() + Conductor.maestro.cleanupCountdowns() } fileprivate func _onAppear() { diff --git a/LeCountdown/Model/Model+Extensions.swift b/LeCountdown/Model/Model+Extensions.swift index 7e8ebdd..0b927fc 100644 --- a/LeCountdown/Model/Model+Extensions.swift +++ b/LeCountdown/Model/Model+Extensions.swift @@ -47,11 +47,11 @@ extension AbstractTimer { extension AbstractSoundTimer { var coolSound: Sound { - return Sound.allCases[Int(self.sound)] + return Sound(rawValue: Int(self.sound)) ?? Sound.allCases[0] } var soundName: String { - coolSound.soundName + return self.coolSound.soundName } } @@ -84,6 +84,10 @@ extension Alarm { extension Stopwatch { + var coolSound: Sound? { + return Sound(rawValue: Int(self.sound)) ?? nil + } + static func fake(context: NSManagedObjectContext) -> Stopwatch { let stopwatch = Stopwatch(context: context) let activity = Activity(context: context) diff --git a/LeCountdown/Sound/Sound.swift b/LeCountdown/Sound/Sound.swift index 260116f..3d780d0 100644 --- a/LeCountdown/Sound/Sound.swift +++ b/LeCountdown/Sound/Sound.swift @@ -13,7 +13,7 @@ enum Sound : Int, CaseIterable, Identifiable { var id: Int { return self.rawValue } - case trainhorn // default + case trainhorn = 1 // default case forestStream var localizedString: String { diff --git a/LeCountdown/Sound/SoundPlayer.swift b/LeCountdown/Sound/SoundPlayer.swift index 5dbcd6c..0eee38d 100644 --- a/LeCountdown/Sound/SoundPlayer.swift +++ b/LeCountdown/Sound/SoundPlayer.swift @@ -33,7 +33,7 @@ enum SoundPlayerError : Error { case badFileName(name: String) } -class SoundPlayer { +@objc class SoundPlayer: NSObject, AVAudioPlayerDelegate { fileprivate var _player: AVAudioPlayer? @@ -42,15 +42,16 @@ class SoundPlayer { throw SoundPlayerError.missingResourceError(file: soundFile) } -// let audioSession: AVAudioSession = AVAudioSession.sharedInstance() -// try audioSession.setCategory(.playback) -// try audioSession.setActive(true) + let audioSession: AVAudioSession = AVAudioSession.sharedInstance() + try audioSession.setCategory(.playback) + try audioSession.setActive(true) _player = try AVAudioPlayer(contentsOf: url) _player?.prepareToPlay() // let loopCount = repeats ? Int.max : 0 _player?.numberOfLoops = 0 //loopCount _player?.volume = 1.0 + _player?.delegate = self // do { // try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, .defaultToSpeaker]) @@ -60,10 +61,17 @@ class SoundPlayer { _player?.play() + } func stop() { self._player?.stop() } + // MARK: - Delegate + + func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + + } + } diff --git a/LeCountdown/TimerRouter.swift b/LeCountdown/TimerRouter.swift index 536548a..74a02ff 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)") } @@ -60,20 +60,11 @@ class TimerRouter { } fileprivate static func _startStopwatch(_ stopwatch: Stopwatch, handler: @escaping (Result) -> Void) { - - Conductor.maestro.currentStopwatches[stopwatch.stringId] = Date() - + Conductor.maestro.startStopwatch(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: end ?? Date())) - } catch { - print("could not record") - } - } + fileprivate static func _stopStopwatch(_ stopwatch: Stopwatch, end: Date? = nil) { + Conductor.maestro.stopStopwatch(stopwatch) } } diff --git a/LeCountdown/Views/Components/GreenCheckmarkView.swift b/LeCountdown/Views/Components/GreenCheckmarkView.swift new file mode 100644 index 0000000..e386613 --- /dev/null +++ b/LeCountdown/Views/Components/GreenCheckmarkView.swift @@ -0,0 +1,23 @@ +// +// GreenCheckmarkView.swift +// LeCountdown +// +// Created by Laurent Morvillier on 06/02/2023. +// + +import SwiftUI + +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) + } +} + +struct GreenCheckmarkView_Previews: PreviewProvider { + static var previews: some View { + GreenCheckmarkView() + } +} diff --git a/LeCountdown/Views/ContentView.swift b/LeCountdown/Views/ContentView.swift index 3ff545d..30832f9 100644 --- a/LeCountdown/Views/ContentView.swift +++ b/LeCountdown/Views/ContentView.swift @@ -88,12 +88,6 @@ struct ContentView: View { self._newView(isPresented: $boringContext.isShowingNewData) .environment(\.managedObjectContext, viewContext) }) -// .sheet(isPresented: $boringContext.isShowingNewData, content: { -// NewStopwatchView(isPresented: $boringContext.isShowingNewData) .environment(\.managedObjectContext, viewContext) -// }) -// .sheet(isPresented: $boringContext.isShowingNewData, content: { -// NewAlarmView(isPresented: $boringContext.isShowingNewData) .environment(\.managedObjectContext, viewContext) -// }) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { diff --git a/LeCountdown/Views/LiveTimerListView.swift b/LeCountdown/Views/LiveTimerListView.swift index 82a3430..e9c60d6 100644 --- a/LeCountdown/Views/LiveTimerListView.swift +++ b/LeCountdown/Views/LiveTimerListView.swift @@ -7,15 +7,6 @@ import SwiftUI -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 @@ -25,7 +16,8 @@ class LiveStopwatchModel: ObservableObject { let now = Date() self.endDate = now - TimerRouter.stopStopwatch(stopwatch, end: now) + Conductor.maestro.stopStopwatch(stopwatch) + } }