diff --git a/LaunchWidget/SingleCountdownView.swift b/LaunchWidget/SingleCountdownView.swift index 544768c..bc424f0 100644 --- a/LaunchWidget/SingleCountdownView.swift +++ b/LaunchWidget/SingleCountdownView.swift @@ -17,9 +17,18 @@ struct SingleCountdownView: View { var body: some View { VStack { - Text(countdown.name ?? "") - Text(countdown.duration.minuteSecond) - }.foregroundColor(Color.white) + HStack { + VStack(alignment: .leading) { + Text(countdown.displayName.uppercased()) + Text(countdown.duration.minuteSecond) + } + Spacer() + } + Spacer() + } + .padding() + .monospaced() + .foregroundColor(Color.white) .font(self.font) .widgetURL(countdown.url) } @@ -27,7 +36,7 @@ struct SingleCountdownView: View { private var font: Font { switch family { case .systemSmall, .systemMedium, .systemLarge, .systemExtraLarge: - return .title2 + return .body default: return .body } @@ -39,6 +48,11 @@ struct MultiCountdownView: View { @Environment(\.widgetFamily) var family: WidgetFamily + private let columns: [GridItem] = [ + GridItem(spacing: 10.0), + GridItem(spacing: 10.0), + ] + var countdowns: [Countdown] var body: some View { @@ -46,20 +60,52 @@ struct MultiCountdownView: View { if countdowns.isEmpty { VoidView() } else { - HStack { + + LazyVGrid( + columns: columns, + spacing: 10.0 + ) { + ForEach(countdowns) { countdown in Link(destination: countdown.url) { - VStack { - Text(countdown.name ?? "") - Text(countdown.duration.minuteSecond) + HStack { + + VStack(alignment: .leading) { + Spacer() + Text(countdown.displayName.uppercased()) + Text(countdown.duration.minuteSecond) + Spacer() + } + Spacer() } - .font(self.font) - .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(.horizontal) + .font(.callout) + .background(.cyan) + .foregroundColor(.white) + .monospaced() + .cornerRadius(16.0) } - + } - }.frame(maxWidth: .infinity) + }.padding() + + + +// HStack { +// ForEach(countdowns) { countdown in +// +// Link(destination: countdown.url) { +// VStack { +// Text(countdown.name ?? "") +// Text(countdown.duration.minuteSecond) +// } +// .font(self.font) +// .frame(maxWidth: .infinity, maxHeight: .infinity) +// } +// +// } +// }.frame(maxWidth: .infinity) } } @@ -78,7 +124,7 @@ struct MultiCountdownView: View { struct CountdownView_Previews: PreviewProvider { static var previews: some View { - SingleCountdownView(countdown: Countdown.fake(context: PersistenceController.preview.container.viewContext)).previewContext(WidgetPreviewContext(family: .systemSmall)) + SingleCountdownView(countdown: Countdown.fake(context: PersistenceController.preview.container.viewContext)).previewContext(WidgetPreviewContext(family: .systemSmall)).background(.black) SingleCountdownView(countdown: Countdown.fake(context: PersistenceController.preview.container.viewContext)).previewContext(WidgetPreviewContext(family: .accessoryRectangular)) MultiCountdownView(countdowns: self.countdowns(context: PersistenceController.preview.container.viewContext)).previewContext(WidgetPreviewContext(family: .systemMedium)) } diff --git a/LeCountdown.xcodeproj/project.pbxproj b/LeCountdown.xcodeproj/project.pbxproj index 3fb1af9..66c4392 100644 --- a/LeCountdown.xcodeproj/project.pbxproj +++ b/LeCountdown.xcodeproj/project.pbxproj @@ -64,6 +64,8 @@ C4742B5F2984205000D5D950 /* ViewModifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4742B5E2984205000D5D950 /* ViewModifiers.swift */; }; C4F8B1532987FE6F005C86A5 /* LaunchWidgetLiveActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7D72981216200BF3EF9 /* LaunchWidgetLiveActivity.swift */; }; C4F8B1552988751B005C86A5 /* DialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4F8B1542988751B005C86A5 /* DialView.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 */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -162,6 +164,8 @@ C4742B5A298414B000D5D950 /* ImageSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSelectionView.swift; sourceTree = ""; }; C4742B5E2984205000D5D950 /* ViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifiers.swift; sourceTree = ""; }; C4F8B1542988751B005C86A5 /* DialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DialView.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 = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -237,6 +241,7 @@ C438C7C829803CA000BF3EF9 /* AppDelegate.swift */, C4060DBF297AE73B003FAB80 /* LeCountdownApp.swift */, C438C7C02980228B00BF3EF9 /* CountdownScheduler.swift */, + C4F8B15629891271005C86A5 /* Conductor.swift */, C438C80B2981DE2E00BF3EF9 /* Views */, C438C8092981DDF800BF3EF9 /* Model */, C445FA8D2987B82E0054D761 /* Sound */, @@ -366,6 +371,7 @@ C445FA962987D0CF0054D761 /* Sound_Assets */ = { isa = PBXGroup; children = ( + C4F8B15829891528005C86A5 /* forest_stream.mp3 */, C445FA942987D01C0054D761 /* train_horn.mp3 */, ); path = Sound_Assets; @@ -521,6 +527,7 @@ buildActionMask = 2147483647; files = ( C4060DC7297AE73D003FAB80 /* Preview Assets.xcassets in Resources */, + C4F8B15929891528005C86A5 /* forest_stream.mp3 in Resources */, C445FA952987D01C0054D761 /* train_horn.mp3 in Resources */, C4060DC4297AE73D003FAB80 /* Assets.xcassets in Resources */, ); @@ -568,6 +575,7 @@ C438C80D2982847300BF3EF9 /* CoreDataRequests.swift in Sources */, C4F8B1552988751B005C86A5 /* DialView.swift in Sources */, C4742B5F2984205000D5D950 /* ViewModifiers.swift in Sources */, + C4F8B15729891271005C86A5 /* Conductor.swift in Sources */, C438C81129829EAF00BF3EF9 /* PropertyWrappers.swift in Sources */, C438C807298195E600BF3EF9 /* Model+Extensions.swift in Sources */, C438C7FF2981300500BF3EF9 /* IntentDataProvider.swift in Sources */, diff --git a/LeCountdown/AppDelegate.swift b/LeCountdown/AppDelegate.swift index 0418b64..d91be40 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 - AppEnvironment.sun.cleanup() + Conductor.maestro.cleanup() return true } @@ -25,14 +25,14 @@ extension AppDelegate: UNUserNotificationCenterDelegate { print("didReceive notification") - AppEnvironment.sun.stopSoundIfNecessary() +// Conductor.maestro.stopSoundIfNecessary() } func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { print("willPresent notification") completionHandler([.banner]) - AppEnvironment.sun.notifyUser(countdownId: notification.request.identifier, cancel: false) + Conductor.maestro.notifyUser(countdownId: notification.request.identifier, cancel: false) } diff --git a/LeCountdown/Conductor.swift b/LeCountdown/Conductor.swift new file mode 100644 index 0000000..7465b23 --- /dev/null +++ b/LeCountdown/Conductor.swift @@ -0,0 +1,133 @@ +// +// AppEnvironment.swift +// LeCountdown +// +// Created by Laurent Morvillier on 31/01/2023. +// + +import Foundation +import ActivityKit + +class Conductor : ObservableObject { + + static let maestro: Conductor = Conductor() + + @UserDefault(Key.dates.rawValue, defaultValue: [:]) static var savedDates: [String : DateInterval] + + init() { + self.notificationDates = Conductor.savedDates + } + + @Published var notificationDates: [String : DateInterval] = [:] { + didSet { + Conductor.savedDates = notificationDates + } + } + + enum Key : String { + case dates + } + + func startCountdown(_ date: Date, countdown: Countdown) { + DispatchQueue.main.async { + let dateInterval = DateInterval(start: Date(), end: date) + self.notificationDates[countdown.stringId] = dateInterval + + self._launchLiveActivity(countdown: countdown, endDate: date) + } + } + + func notifyUser(countdownId: String, cancel: Bool) { +// self._playSound(countdownId: countdownId) + endCountdown(countdownId: countdownId, cancel: cancel) + } + + func endCountdown(countdownId: String, cancel: Bool) { + DispatchQueue.main.async { + if !cancel { + self._recordActivity(countdownId: countdownId) + } + self.notificationDates.removeValue(forKey: countdownId) + + self._endLiveActivity(countdownId: countdownId) + } + } + + func cleanup() { + let now = Date() + for (key, value) in self.notificationDates { + if value.end < now { + self.endCountdown(countdownId: key, 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.notificationDates[countdownId] { + do { + try CoreDataRequests.recordActivity(countdown: countdown, dateInterval: dateInterval) + } catch { + print("Could not record activity = \(error)") + // TODO: show error to user + } + } + } + + // MARK: - Live Activity + + fileprivate func _launchLiveActivity(countdown: Countdown, endDate: 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)) + + do { + let liveActivity = try ActivityKit.Activity.request(attributes: attributes, content: activityContent) + print("Requested a countdown Live Activity \(String(describing: liveActivity.id)).") + } catch (let error) { + print("Error requesting countdown Live Activity \(error.localizedDescription).") + } + + } + } + + fileprivate func _liveActivity(countdownId: String) -> ActivityKit.Activity? { + return ActivityKit.Activity.activities.first(where: { $0.attributes.id == countdownId } ) + } + + func updateLiveActivities() { + + for (countdownId, interval) in self.notificationDates { + if let activity = self._liveActivity(countdownId: countdownId) { + + Task { + let ended = interval.end < Date() + 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(countdownId: String) { + + print("Trt to end the Live Activity: \(countdownId)") + + if let activity = self._liveActivity(countdownId: countdownId) { + Task { + let state = LaunchWidgetAttributes.ContentState(ended: true) + let content = ActivityContent(state: state, staleDate: Date()) + await activity.end(content, dismissalPolicy: .immediate) + print("Ending the Live Activity: \(activity.id)") + } + } + } + +} diff --git a/LeCountdown/CountdownScheduler.swift b/LeCountdown/CountdownScheduler.swift index 52c21c8..dc41d42 100644 --- a/LeCountdown/CountdownScheduler.swift +++ b/LeCountdown/CountdownScheduler.swift @@ -7,7 +7,6 @@ import Foundation import UserNotifications -import ActivityKit class CountdownScheduler { @@ -20,7 +19,7 @@ class CountdownScheduler { func cancelCurrentNotifications(countdown: Countdown) { UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [countdown.stringId]) - AppEnvironment.sun.endCountdown(countdownId: countdown.stringId, cancel: true) + Conductor.maestro.endCountdown(countdownId: countdown.stringId, cancel: true) } fileprivate func _scheduleCountdownNotification(countdown: Countdown, handler: @escaping (Result) -> Void) { @@ -36,10 +35,25 @@ class CountdownScheduler { } content.body = body - content.sound = UNNotificationSound.defaultCritical + content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: countdown.soundName)) + content.interruptionLevel = .critical + + self._createNotification(countdown: countdown, content: content, handler: handler) + +// if countdown.repeats { +// for i in 1...50 { +// let offset = Double(i) * (countdown.coolSound.duration + 1.0) +//// self._createNotification(countdown: countdown, offset: offset, content: content, handler: handler) +// } +// } + + } + + fileprivate func _createNotification(countdown: Countdown, offset: TimeInterval = 0.0, content: UNMutableNotificationContent, handler: @escaping (Result) -> Void) { - let trigger = UNTimeIntervalNotificationTrigger(timeInterval: duration, repeats: false) - let request = UNNotificationRequest(identifier: countdown.objectID.uriRepresentation().absoluteString, + let duration = countdown.duration + let trigger = UNTimeIntervalNotificationTrigger(timeInterval: duration + offset, repeats: false) + let request = UNNotificationRequest(identifier: countdown.objectID.uriRepresentation().absoluteString + "/\(offset)", content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) { error in @@ -48,12 +62,15 @@ class CountdownScheduler { handler(.failure(error)) print("Scheduling error = \(error)") } else { - if let triggerDate = trigger.nextTriggerDate() { - AppEnvironment.sun.startCountdown(triggerDate, countdown: countdown) - handler(.success(trigger.nextTriggerDate())) - } else { - let backupDate = Date().addingTimeInterval(duration) - AppEnvironment.sun.startCountdown(backupDate, countdown: countdown) + + if offset == 0.0 { + if let triggerDate = trigger.nextTriggerDate() { + Conductor.maestro.startCountdown(triggerDate, countdown: countdown) + handler(.success(trigger.nextTriggerDate())) + } else { + let backupDate = Date().addingTimeInterval(duration) + Conductor.maestro.startCountdown(backupDate, countdown: countdown) + } } } } @@ -62,152 +79,3 @@ class CountdownScheduler { } } - -class AppEnvironment : ObservableObject { - - static let sun: AppEnvironment = AppEnvironment() - - var soundPlayer: SoundPlayer? = nil - - @UserDefault(Key.dates.rawValue, defaultValue: [:]) static var savedDates: [String : DateInterval] - - init() { - self.notificationDates = AppEnvironment.savedDates - } - - @Published var notificationDates: [String : DateInterval] = [:] { - didSet { - AppEnvironment.savedDates = notificationDates - } - } - - enum Key : String { - case dates - } - - func startCountdown(_ date: Date, countdown: Countdown) { - DispatchQueue.main.async { - let dateInterval = DateInterval(start: Date(), end: date) - self.notificationDates[countdown.stringId] = dateInterval - - self._launchLiveActivity(countdown: countdown, endDate: date) - } - } - - func notifyUser(countdownId: String, cancel: Bool) { - self._playSound(countdownId: countdownId) - endCountdown(countdownId: countdownId, cancel: cancel) - } - - func endCountdown(countdownId: String, cancel: Bool) { - DispatchQueue.main.async { - if !cancel { - self._recordActivity(countdownId: countdownId) - } - self.notificationDates.removeValue(forKey: countdownId) - -// self._updateLiveActivity(countdownId: countdownId, endDate: <#T##Date#>) - self._endLiveActivity(countdownId: countdownId) - } - } - - func cleanup() { - let now = Date() - for (key, value) in self.notificationDates { - if value.end < now { - self.endCountdown(countdownId: key, 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.notificationDates[countdownId] { - do { - try CoreDataRequests.recordActivity(countdown: countdown, dateInterval: dateInterval) - } catch { - print("Could not record activity = \(error)") - // TODO: show error to user - } - } - } - - // MARK: - Sound - - fileprivate func _playSound(countdownId: String) { - - let countdown = PersistenceController.shared.container.viewContext.object(stringId: countdownId) as? Countdown - let soundFile = countdown?.soundFile ?? Sound.allCases[0].soundFile - - let soundPlayer = SoundPlayer() - self.soundPlayer = soundPlayer - do { - try soundPlayer.playSound(soundFile: soundFile, repeats: countdown?.repeats ?? true) - } catch { - print("error = \(error)") - // TODO: manage error - } - - } - - func stopSoundIfNecessary() { - self.soundPlayer?.stop() - } - - // MARK: - Live Activity - - fileprivate func _launchLiveActivity(countdown: Countdown, endDate: 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)) - - do { - let liveActivity = try ActivityKit.Activity.request(attributes: attributes, content: activityContent) - print("Requested a countdown Live Activity \(String(describing: liveActivity.id)).") - } catch (let error) { - print("Error requesting countdown Live Activity \(error.localizedDescription).") - } - - } - } - - fileprivate func _liveActivity(countdownId: String) -> ActivityKit.Activity? { - return ActivityKit.Activity.activities.first(where: { $0.attributes.id == countdownId } ) - } - - func updateLiveActivities() { - - for (countdownId, interval) in self.notificationDates { - if let activity = self._liveActivity(countdownId: countdownId) { - - Task { - let ended = interval.end < Date() - 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(countdownId: String) { - - print("Trt to end the Live Activity: \(countdownId)") - - if let activity = self._liveActivity(countdownId: countdownId) { - Task { - let state = LaunchWidgetAttributes.ContentState(ended: true) - let content = ActivityContent(state: state, staleDate: Date()) - await activity.end(content, dismissalPolicy: .immediate) - print("Ending the Live Activity: \(activity.id)") - } - } - } - -} diff --git a/LeCountdown/Info.plist b/LeCountdown/Info.plist index 3847eec..aba22b9 100644 --- a/LeCountdown/Info.plist +++ b/LeCountdown/Info.plist @@ -13,5 +13,9 @@ UISceneConfigurations + UIBackgroundModes + + audio + diff --git a/LeCountdown/LeCountdownApp.swift b/LeCountdown/LeCountdownApp.swift index 1506ee6..0b72773 100644 --- a/LeCountdown/LeCountdownApp.swift +++ b/LeCountdown/LeCountdownApp.swift @@ -17,7 +17,7 @@ struct LeCountdownApp: App { var body: some Scene { WindowGroup { ContentView() - .environmentObject(AppEnvironment.sun) + .environmentObject(Conductor.maestro) .environment(\.managedObjectContext, persistenceController.container.viewContext) .onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in self._willEnterForegroundNotification() @@ -28,11 +28,20 @@ struct LeCountdownApp: App { } fileprivate func _willEnterForegroundNotification() { - AppEnvironment.sun.cleanup() + Conductor.maestro.cleanup() } fileprivate func _onAppear() { - + Task { + for s in Sound.allCases { + do { + let d = try await s.duration() + print("\(s) duration = \(d)") + } catch { + print("error = \(error)") + } + } + } } } diff --git a/LeCountdown/Model/Model+Extensions.swift b/LeCountdown/Model/Model+Extensions.swift index 83dcb3e..d87bcca 100644 --- a/LeCountdown/Model/Model+Extensions.swift +++ b/LeCountdown/Model/Model+Extensions.swift @@ -38,10 +38,18 @@ extension Countdown { return self.coolpic.rawValue } - var soundFile: SoundFile { - return Sound.allCases[Int(self.sound)].soundFile + var coolSound: Sound { + return Sound.allCases[Int(self.sound)] } + var soundName: String { + coolSound.soundName + } + +// var soundFile: SoundFile { +// return Sound.allCases[Int(self.sound)].soundFile +// } + static func fake(context: NSManagedObjectContext) -> Countdown { let cd = Countdown(context: context) cd.duration = 4 * 60.0 diff --git a/LeCountdown/Sound/Sound.swift b/LeCountdown/Sound/Sound.swift index 02999c5..dbc8b35 100644 --- a/LeCountdown/Sound/Sound.swift +++ b/LeCountdown/Sound/Sound.swift @@ -6,15 +6,7 @@ // import Foundation - -struct SoundFile { - var filename: String - var fileExtension: String - - var url: URL? { - return Bundle.main.url(forResource: self.filename, withExtension: self.fileExtension) - } -} +import AVFoundation // Sound id are stored thus case order should not be changed enum Sound : Int, CaseIterable, Identifiable { @@ -22,17 +14,57 @@ enum Sound : Int, CaseIterable, Identifiable { var id: Int { return self.rawValue } case trainhorn // default + case forestStream var localizedString: String { switch self { case .trainhorn: return NSLocalizedString("Train horn", comment: "") + case .forestStream: return NSLocalizedString("Forest stream", comment: "") + } + } + + var soundName: String { + switch self { + case .trainhorn: return "train_horn.mp3" + case .forestStream: return "forest_stream.mp3" } } - var soundFile: SoundFile { + var duration: TimeInterval { switch self { - case .trainhorn: return SoundFile(filename: "train_horn", fileExtension: "mp3") + case .trainhorn: return 7.8 + case .forestStream: return 300.1 + } + } + + var url: URL? { + + let components = self.soundName.components(separatedBy: ".") + if components.count == 2 { + return Bundle.main.url(forResource: components[0], withExtension: components[1]) + } else { + print("bad sound file name for \(self)") + return nil + } + + } + +// var soundFile: SoundFile { +// switch self { +// case .trainhorn: return SoundFile(filename: "train_horn", fileExtension: "mp3") +// } +// } + + func duration() async throws -> TimeInterval { + + guard let url = self.url else { + print("sound \(self) has no url") + return -1.0 } + + let audioAsset = AVURLAsset.init(url: url, options: nil) + let duration = try await audioAsset.load(.duration) + return CMTimeGetSeconds(duration) } } diff --git a/LeCountdown/Sound/SoundPlayer.swift b/LeCountdown/Sound/SoundPlayer.swift index 6ddd3c5..a18c4d1 100644 --- a/LeCountdown/Sound/SoundPlayer.swift +++ b/LeCountdown/Sound/SoundPlayer.swift @@ -8,6 +8,15 @@ import Foundation import AVFoundation +struct SoundFile { + var filename: String + var fileExtension: String + + var url: URL? { + return Bundle.main.url(forResource: self.filename, withExtension: self.fileExtension) + } +} + enum SoundPlayerError : Error { case missingResourceError(file: SoundFile) } @@ -20,6 +29,10 @@ class SoundPlayer { guard let url = soundFile.url else { throw SoundPlayerError.missingResourceError(file: soundFile) } + + let audioSession: AVAudioSession = AVAudioSession.sharedInstance() + try audioSession.setCategory(.playback) + try audioSession.setActive(true) _player = try AVAudioPlayer(contentsOf: url) _player?.prepareToPlay() diff --git a/LeCountdown/Sound_Assets/forest_stream.mp3 b/LeCountdown/Sound_Assets/forest_stream.mp3 new file mode 100644 index 0000000..b1bda28 Binary files /dev/null and b/LeCountdown/Sound_Assets/forest_stream.mp3 differ diff --git a/LeCountdown/Views/ContentView.swift b/LeCountdown/Views/ContentView.swift index 29f3229..e50c0d8 100644 --- a/LeCountdown/Views/ContentView.swift +++ b/LeCountdown/Views/ContentView.swift @@ -10,7 +10,7 @@ import CoreData struct CountdownLiveView: View { - @EnvironmentObject var environment: AppEnvironment + @EnvironmentObject var environment: Conductor @ObservedObject var countdown: Countdown @@ -50,7 +50,7 @@ struct CountdownLiveView: View { struct ContentView: View { - @EnvironmentObject var environment: AppEnvironment + @EnvironmentObject var environment: Conductor @Environment(\.managedObjectContext) private var viewContext @@ -90,7 +90,7 @@ struct ContentView: View { } label: { CountdownLiveView(countdown: countdown) - .environmentObject(AppEnvironment.sun) + .environmentObject(Conductor.maestro) .aspectRatio(contentMode: .fill) .frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity) .aspectRatio(1, contentMode: .fit) @@ -206,7 +206,7 @@ struct ContentView: View { fileprivate extension Countdown { var endDate: Date? { - return AppEnvironment.sun.notificationDates[self.stringId]?.end + return Conductor.maestro.notificationDates[self.stringId]?.end } var isLive: Bool { diff --git a/LeCountdown/Views/CountdownFormView.swift b/LeCountdown/Views/CountdownFormView.swift index d33e88c..c6f82da 100644 --- a/LeCountdown/Views/CountdownFormView.swift +++ b/LeCountdown/Views/CountdownFormView.swift @@ -58,11 +58,11 @@ struct CountdownFormView : View { } else { Image(imageBinding.wrappedValue.rawValue) } - } .font(Font.system(size: 90.0)) - .aspectRatio(1, contentMode: .fit) - .frame(width: 100.0, height: 100.0) - .cornerRadius(40.0) - + } + .font(Font.system(size: 90.0)) + .aspectRatio(1, contentMode: .fit) + .frame(width: 100.0, height: 100.0) + .cornerRadius(40.0) } } diff --git a/LeCountdown/Views/DialView.swift b/LeCountdown/Views/DialView.swift index 2e2c7b7..3638a20 100644 --- a/LeCountdown/Views/DialView.swift +++ b/LeCountdown/Views/DialView.swift @@ -9,7 +9,7 @@ import SwiftUI struct DialView: View { - @EnvironmentObject var environment: AppEnvironment + @EnvironmentObject var environment: Conductor var name: String var duration: String diff --git a/LeCountdown/Views/NewCountdownView.swift b/LeCountdown/Views/NewCountdownView.swift index 4512956..002924e 100644 --- a/LeCountdown/Views/NewCountdownView.swift +++ b/LeCountdown/Views/NewCountdownView.swift @@ -57,13 +57,14 @@ struct CountdownEditView : View { var body: some View { NavigationStack { - CountdownFormView(secondsBinding: $secondsString, - minutesBinding: $minutesString, - nameBinding: $nameString, - imageBinding: $image, - soundBinding: $sound, - repeatsBinding: $soundRepeats, - textFieldIsFocused: $textFieldIsFocused) + CountdownFormView( + secondsBinding: $secondsString, + minutesBinding: $minutesString, + nameBinding: $nameString, + imageBinding: $image, + soundBinding: $sound, + repeatsBinding: $soundRepeats, + textFieldIsFocused: $textFieldIsFocused) .onAppear { self._onAppear() }