From c1bdac59ff87e278a24f722f2c0b966fa70c4865 Mon Sep 17 00:00:00 2001 From: Laurent Date: Thu, 6 Apr 2023 17:07:27 +0200 Subject: [PATCH] Live activities and various improvements --- LaunchWidget/LaunchWidgetLiveActivity.swift | 31 ++--- LeCountdown/Conductor.swift | 62 +++++----- LeCountdown/Model/LiveTimer.swift | 1 + LeCountdown/Sound/DelaySoundPlayer.swift | 26 +--- .../Subscription/SubscriptionButtonView.swift | 1 + .../Views/Countdown/NewCountdownView.swift | 2 +- LeCountdown/Views/LiveTimerListView.swift | 114 +++++++++--------- .../Widget/LaunchWidgetAttributes.swift | 1 + LeCountdown/fr.lproj/Localizable.strings | 2 +- 9 files changed, 111 insertions(+), 129 deletions(-) diff --git a/LaunchWidget/LaunchWidgetLiveActivity.swift b/LaunchWidget/LaunchWidgetLiveActivity.swift index 25523fb..4034d3b 100644 --- a/LaunchWidget/LaunchWidgetLiveActivity.swift +++ b/LaunchWidget/LaunchWidgetLiveActivity.swift @@ -32,33 +32,26 @@ struct LaunchWidgetLiveActivity: Widget { var body: some WidgetConfiguration { ActivityConfiguration(for: LaunchWidgetAttributes.self) { context in -// let range = Date()...context.attributes.date - // Lock screen/banner UI goes here HStack { Text(context.attributes.name.uppercased()) Spacer() - 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) -// .monospaced() -// } else { -// Text("It's time!") -// } + if context.attributes.isTimer { + let range = Date()...context.attributes.date + Text(timerInterval: range, + pauseTime: range.lowerBound) + .font(.title) + } else { + Text(context.attributes.date, style: .timer) + .font(.title) + } + }.padding() .monospaced() .foregroundColor(.white) - .activityBackgroundTint(Color(white: 0.2)) + .activityBackgroundTint(Color(red: 1.0, green: 0.5, blue: 0.4)) .activitySystemActionForegroundColor(.white) - } dynamicIsland: { context in DynamicIsland { // Expanded UI goes here. Compose the expanded UI through @@ -100,7 +93,7 @@ struct LaunchWidgetLiveActivity_Previews: PreviewProvider { static let attributes = LaunchWidgetAttributes( id: "", name: "Tea", - date: Date().addingTimeInterval(3600.0)) + date: Date().addingTimeInterval(3600.0), isTimer: true) static let contentState = LaunchWidgetAttributes.ContentState(ended: false) diff --git a/LeCountdown/Conductor.swift b/LeCountdown/Conductor.swift index 211da46..575f799 100644 --- a/LeCountdown/Conductor.swift +++ b/LeCountdown/Conductor.swift @@ -64,6 +64,7 @@ class Conductor: ObservableObject { func removeLiveTimer(id: TimerID) { // Logger.log("removeLiveTimer") self.liveTimers.removeAll(where: { $0.id == id }) + self.currentStopwatches.removeValue(forKey: id) self.cancelledCountdowns.removeAll(where: { $0 == id }) if let soundPlayer = self._delayedSoundPlayers[id] { soundPlayer.stop() @@ -93,7 +94,7 @@ class Conductor: ObservableObject { } let liveStopwatches: [LiveTimer] = self.currentStopwatches.map { - return LiveTimer(id: $0, date: $1.start) + return LiveTimer(id: $0, date: $1.start, endDate: $1.end) } for liveStopwatch in liveStopwatches { if let index = self.liveTimers.firstIndex(where: { $0.id == liveStopwatch.id }) { @@ -167,7 +168,8 @@ class Conductor: ObservableObject { if Preferences.playConfirmationSound { self._playConfirmationSound(timer: countdown) } - + self._launchLiveActivity(timer: countdown, date: date) + handler(.success(date)) } catch { Logger.error(error) @@ -181,13 +183,19 @@ class Conductor: ObservableObject { func startStopwatch(_ stopwatch: Stopwatch) { DispatchQueue.main.async { let lsw = LiveStopWatch(start: Date()) - Conductor.maestro.currentStopwatches[stopwatch.stringId] = lsw + +// if let liveTimer = liveTimers.first(where: { $0.id == stopwatch.stringId }) { +// liveTimer. +// } + + self.currentStopwatches[stopwatch.stringId] = lsw if Preferences.playConfirmationSound { self._playSound(Const.confirmationSound.rawValue) } - self._launchLiveActivity(stopwatch: stopwatch, start: lsw.start) + self._endLiveActivity(timerId: stopwatch.stringId) + self._launchLiveActivity(timer: stopwatch, date: lsw.start) // self._createTimerIntent(stopwatch) @@ -200,7 +208,9 @@ class Conductor: ObservableObject { if lsw.end == nil { let end = Date() lsw.end = end - // Conductor.maestro.currentStopwatches.removeValue(forKey: stopwatch.stringId) + + Conductor.maestro.currentStopwatches[stopwatch.stringId] = lsw + do { try CoreDataRequests.recordActivity(timer: stopwatch, dateInterval: DateInterval(start: lsw.start, end: end)) } catch { @@ -238,6 +248,7 @@ class Conductor: ObservableObject { func cleanup() { self._cleanupCountdowns() + self.cleanupLiveActivities() withAnimation { self._cleanupLiveTimers() @@ -368,19 +379,19 @@ class Conductor: ObservableObject { // MARK: - Live Activity - fileprivate func _launchLiveActivity(stopwatch: Stopwatch, start: Date) { + fileprivate func _launchLiveActivity(timer: AbstractTimer, date: Date) { if #available(iOS 16.2, *) { if ActivityAuthorizationInfo().areActivitiesEnabled { let contentState = LaunchWidgetAttributes.ContentState(ended: false) - let attributes = LaunchWidgetAttributes(id: stopwatch.stringId, name: stopwatch.displayName, date: start) + let attributes = LaunchWidgetAttributes(id: timer.stringId, name: timer.displayName, date: date, isTimer: timer is Countdown) let activityContent = ActivityContent(state: contentState, staleDate: nil) do { let liveActivity = try ActivityKit.Activity.request(attributes: attributes, content: activityContent) - print("Requested a Live Activity: \(String(describing: liveActivity.id)).") + print("Requested a Live Activity: \(String(describing: liveActivity.id))") } catch (let error) { Logger.error(error) } @@ -392,41 +403,36 @@ class Conductor: ObservableObject { } - fileprivate func _liveActivity(timerId: String) -> ActivityKit.Activity? { - return ActivityKit.Activity.activities.first(where: { $0.attributes.id == timerId } ) + fileprivate func _liveActivity(timerId: String) -> [ActivityKit.Activity] { + return ActivityKit.Activity.activities.filter { $0.id == timerId } + } + + fileprivate func _liveActivityIds() -> [String] { + return ActivityKit.Activity.activities.map { $0.id } + } + + func cleanupLiveActivities() { + for id in self._liveActivityIds() { + if self.liveTimers.first(where: { $0.id == id} ) == nil { + self._endLiveActivity(timerId: id) + } + } } func updateLiveActivities() { print("update live activity...") for (countdownId, interval) in self.currentCountdowns { - if interval.end < Date() { self._endLiveActivity(timerId: countdownId) } - -// if let activity = self._liveActivity(countdownId: countdownId) { -// -// Task { -// -// if ended { -// self._endLiveActivity(countdownId: countdownId) -// } -// -//// let state = LaunchWidgetAttributes.ContentState(ended: ended) -//// let content = ActivityContent(state: state, staleDate: interval.end) -//// await activity.update(content) -//// print("Ending the Live Activity: \(activity.id)") -// } -// } } - } fileprivate func _endLiveActivity(timerId: String) { if #available(iOS 16.2, *) { print("Try to end the Live Activity: \(timerId)") - if let activity = self._liveActivity(timerId: timerId) { + for activity in self._liveActivity(timerId: timerId) { Task { let state = LaunchWidgetAttributes.ContentState(ended: true) let content = ActivityContent(state: state, staleDate: Date()) diff --git a/LeCountdown/Model/LiveTimer.swift b/LeCountdown/Model/LiveTimer.swift index 99bad8b..a4ec3ab 100644 --- a/LeCountdown/Model/LiveTimer.swift +++ b/LeCountdown/Model/LiveTimer.swift @@ -11,6 +11,7 @@ import CoreData struct LiveTimer: Identifiable, Comparable { var id: String var date: Date + var endDate: Date? static func < (lhs: LiveTimer, rhs: LiveTimer) -> Bool { return lhs.date < rhs.date diff --git a/LeCountdown/Sound/DelaySoundPlayer.swift b/LeCountdown/Sound/DelaySoundPlayer.swift index d1ef88c..a710f0d 100644 --- a/LeCountdown/Sound/DelaySoundPlayer.swift +++ b/LeCountdown/Sound/DelaySoundPlayer.swift @@ -42,10 +42,6 @@ import AVFoundation fileprivate func _play(in duration: TimeInterval, repeatCount: Int) throws { -// let audioSession: AVAudioSession = AVAudioSession.sharedInstance() -// try audioSession.setCategory(.playback, options: .duckOthers) -// try audioSession.setActive(true) - self._player.prepareToPlay() self._player.volume = 1.0 self._player.delegate = self @@ -54,38 +50,18 @@ import AVFoundation Logger.log("self._player.deviceCurrentTime = \(self._player.deviceCurrentTime)") self._player.play(atTime: self._player.deviceCurrentTime + duration) -// if repeatCount == 0 { -// self._scheduleFadeOut(duration: duration) -// } - } func stop() { self._player.stop() } -// fileprivate func _scheduleFadeOut(duration: TimeInterval) { -// Logger.log("_scheduleFadeOut") -// guard let soundDuration = self._soundDuration, soundDuration > 1.0 else { -// return -// } -// -// let interval = duration + soundDuration - 1.0 -// Logger.log("Fade in \(interval)") -// let date = Date(timeIntervalSinceNow: interval) -// Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { _ in -// Logger.log("FADEOUT!") -// self._player.setVolume(0.0, fadeDuration: 1.0) -// } -// self._timer?.fire() -// } - // MARK: - Delegate func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { Logger.log("audioPlayerDidFinishPlaying: successfully = \(flag)") Conductor.maestro.cancelSoundPlayer(id: self._timerID) -// self._player.volume = 1.0 + Conductor.maestro.cleanupLiveActivities() } } diff --git a/LeCountdown/Subscription/SubscriptionButtonView.swift b/LeCountdown/Subscription/SubscriptionButtonView.swift index cdba866..c9c3f76 100644 --- a/LeCountdown/Subscription/SubscriptionButtonView.swift +++ b/LeCountdown/Subscription/SubscriptionButtonView.swift @@ -29,6 +29,7 @@ struct SubscriptionButtonView: View { .buttonStyle(.bordered) .foregroundColor(.white) } + } struct SubscriptionButtonView_Previews: PreviewProvider { diff --git a/LeCountdown/Views/Countdown/NewCountdownView.swift b/LeCountdown/Views/Countdown/NewCountdownView.swift index eb727bc..e4b831e 100644 --- a/LeCountdown/Views/Countdown/NewCountdownView.swift +++ b/LeCountdown/Views/Countdown/NewCountdownView.swift @@ -254,7 +254,7 @@ struct CountdownEditView : View { fileprivate func _loadCountdown(_ countdown: Countdown) { let hours = Int(countdown.duration / 3600.0) - let minutes = countdown.duration - Double(hours * 3600) + let minutes = Int(countdown.duration - Double(hours * 3600)) / 60 let seconds = countdown.duration - Double(hours * 3600) - Double(minutes * 60) if hours > 0 { diff --git a/LeCountdown/Views/LiveTimerListView.swift b/LeCountdown/Views/LiveTimerListView.swift index 94da0ec..8c636e2 100644 --- a/LeCountdown/Views/LiveTimerListView.swift +++ b/LeCountdown/Views/LiveTimerListView.swift @@ -23,27 +23,79 @@ struct TimeView: View { } -struct LiveStopwatchView: View { +struct LiveTimerListView: View { @Environment(\.managedObjectContext) private var viewContext @EnvironmentObject var conductor: Conductor - @State var stopwatch: Stopwatch - @State var stopped: Bool = false + var body: some View { + + VStack { + ForEach(conductor.liveTimers) { liveTimer in + if let timer: AbstractTimer = liveTimer.timer(context: self.viewContext) { + LiveTimerView(timer: timer, date: liveTimer.date, endDate: liveTimer.endDate) + } + } + + }.padding() + + } + + fileprivate func _columnCount() -> Int { + #if os(iOS) + if UIDevice.isPhoneIdiom { + return 18 + } else { + return 3 + } + #else + return 3 + #endif + } + + fileprivate func _columns() -> [GridItem] { + return (0.. Int { - #if os(iOS) - if UIDevice.isPhoneIdiom { - return 18 - } else { - return 3 - } - #else - return 3 - #endif - } - - fileprivate func _columns() -> [GridItem] { - return (0..