Improvement and fixes

release
Laurent 3 years ago
parent a71c534f27
commit b9fde1b0c2
  1. 29
      LaunchWidget/LaunchWidgetLiveActivity.swift
  2. 6
      LeCountdown.xcodeproj/project.pbxproj
  3. 2
      LeCountdown/AppDelegate.swift
  4. 116
      LeCountdown/Conductor.swift
  5. 2
      LeCountdown/LeCountdownApp.swift
  6. 8
      LeCountdown/Model/Model+Extensions.swift
  7. 2
      LeCountdown/Sound/Sound.swift
  8. 16
      LeCountdown/Sound/SoundPlayer.swift
  9. 17
      LeCountdown/TimerRouter.swift
  10. 23
      LeCountdown/Views/Components/GreenCheckmarkView.swift
  11. 6
      LeCountdown/Views/ContentView.swift
  12. 12
      LeCountdown/Views/LiveTimerListView.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)

@ -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 = "<group>"; };
C498E5A0298D543900E90DE0 /* LiveTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveTimer.swift; sourceTree = "<group>"; };
C498E5A2298D720600E90DE0 /* TestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestView.swift; sourceTree = "<group>"; };
C498E5A4299152B400E90DE0 /* GreenCheckmarkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GreenCheckmarkView.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>"; };
@ -530,6 +533,7 @@
isa = PBXGroup;
children = (
C4F8B1D1298BF646005C86A5 /* PermissionAlertView.swift */,
C498E5A4299152B400E90DE0 /* GreenCheckmarkView.swift */,
);
path = Components;
sourceTree = "<group>";
@ -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 */,

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

@ -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<LaunchWidgetAttributes>? {
return ActivityKit.Activity<LaunchWidgetAttributes>.activities.first(where: { $0.attributes.id == countdownId } )
fileprivate func _liveActivity(timerId: String) -> ActivityKit.Activity<LaunchWidgetAttributes>? {
return ActivityKit.Activity<LaunchWidgetAttributes>.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())

@ -64,7 +64,7 @@ struct LeCountdownApp: App {
}
fileprivate func _willEnterForegroundNotification() {
Conductor.maestro.cleanup()
Conductor.maestro.cleanupCountdowns()
}
fileprivate func _onAppear() {

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

@ -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 {

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

@ -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<Bool, Error>) -> 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)
}
}

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

@ -88,12 +88,6 @@ struct ContentView<T : AbstractTimer>: 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 {

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

Loading…
Cancel
Save