diff --git a/LeCountdown.xcodeproj/project.pbxproj b/LeCountdown.xcodeproj/project.pbxproj index e589795..13d38d1 100644 --- a/LeCountdown.xcodeproj/project.pbxproj +++ b/LeCountdown.xcodeproj/project.pbxproj @@ -51,10 +51,14 @@ C438C8182982BFC100BF3EF9 /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4060DC8297AE73D003FAB80 /* Persistence.swift */; }; C438C8192982BFDB00BF3EF9 /* NSManagedContext+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7C4298024E900BF3EF9 /* NSManagedContext+Extensions.swift */; }; C438C81A2982BFF100BF3EF9 /* NSManagedContext+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C438C7C4298024E900BF3EF9 /* NSManagedContext+Extensions.swift */; }; - C445FA86298448720054D761 /* Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4742B5629840F6400D5D950 /* Media.swift */; }; - C445FA87298448730054D761 /* Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4742B5629840F6400D5D950 /* Media.swift */; }; + C445FA86298448720054D761 /* CoolPic.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4742B5629840F6400D5D950 /* CoolPic.swift */; }; + C445FA87298448730054D761 /* CoolPic.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4742B5629840F6400D5D950 /* CoolPic.swift */; }; C445FA882984487F0054D761 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C4060DC3297AE73D003FAB80 /* Assets.xcassets */; }; - C4742B5729840F6400D5D950 /* Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4742B5629840F6400D5D950 /* Media.swift */; }; + C445FA8F2987B83B0054D761 /* SoundPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C445FA8E2987B83B0054D761 /* SoundPlayer.swift */; }; + C445FA922987CC8A0054D761 /* Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = C445FA912987CC8A0054D761 /* Sound.swift */; }; + C445FA932987CF280054D761 /* Sound.swift in Sources */ = {isa = PBXBuildFile; fileRef = C445FA912987CC8A0054D761 /* Sound.swift */; }; + C445FA952987D01C0054D761 /* train_horn.mp3 in Resources */ = {isa = PBXBuildFile; fileRef = C445FA942987D01C0054D761 /* train_horn.mp3 */; }; + C4742B5729840F6400D5D950 /* CoolPic.swift in Sources */ = {isa = PBXBuildFile; fileRef = C4742B5629840F6400D5D950 /* CoolPic.swift */; }; 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 */; }; @@ -147,7 +151,11 @@ C438C80C2982847300BF3EF9 /* CoreDataRequests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreDataRequests.swift; sourceTree = ""; }; C438C80E29828B8600BF3EF9 /* RecordsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordsView.swift; sourceTree = ""; }; C438C81029829EAF00BF3EF9 /* PropertyWrappers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyWrappers.swift; sourceTree = ""; }; - C4742B5629840F6400D5D950 /* Media.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Media.swift; sourceTree = ""; }; + C445FA8E2987B83B0054D761 /* SoundPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundPlayer.swift; sourceTree = ""; }; + C445FA902987C0CF0054D761 /* LeCountdown.0.2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.2.xcdatamodel; sourceTree = ""; }; + C445FA912987CC8A0054D761 /* Sound.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sound.swift; sourceTree = ""; }; + C445FA942987D01C0054D761 /* train_horn.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = train_horn.mp3; sourceTree = ""; }; + C4742B5629840F6400D5D950 /* CoolPic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoolPic.swift; sourceTree = ""; }; C4742B58298411E800D5D950 /* CountdownFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountdownFormView.swift; sourceTree = ""; }; C4742B5A298414B000D5D950 /* ImageSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSelectionView.swift; sourceTree = ""; }; C4742B5E2984205000D5D950 /* ViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifiers.swift; sourceTree = ""; }; @@ -226,11 +234,12 @@ C438C7C829803CA000BF3EF9 /* AppDelegate.swift */, C4060DBF297AE73B003FAB80 /* LeCountdownApp.swift */, C438C7C02980228B00BF3EF9 /* CountdownScheduler.swift */, - C4742B5629840F6400D5D950 /* Media.swift */, C438C80B2981DE2E00BF3EF9 /* Views */, C438C8092981DDF800BF3EF9 /* Model */, + C445FA8D2987B82E0054D761 /* Sound */, C438C80A2981DE1A00BF3EF9 /* Utils */, C438C8082981DDD200BF3EF9 /* Widget */, + C445FA962987D0CF0054D761 /* Sound_Assets */, C4060DC3297AE73D003FAB80 /* Assets.xcassets */, C438C80429813B3100BF3EF9 /* LeCountdown.entitlements */, C4060DCD297AE73D003FAB80 /* Info.plist */, @@ -321,6 +330,7 @@ C438C80A2981DE1A00BF3EF9 /* Utils */ = { isa = PBXGroup; children = ( + C4742B5629840F6400D5D950 /* CoolPic.swift */, C438C81029829EAF00BF3EF9 /* PropertyWrappers.swift */, C4060DF4297AE9A7003FAB80 /* TimeInterval+Extensions.swift */, C4742B5E2984205000D5D950 /* ViewModifiers.swift */, @@ -340,6 +350,23 @@ path = Views; sourceTree = ""; }; + C445FA8D2987B82E0054D761 /* Sound */ = { + isa = PBXGroup; + children = ( + C445FA912987CC8A0054D761 /* Sound.swift */, + C445FA8E2987B83B0054D761 /* SoundPlayer.swift */, + ); + path = Sound; + sourceTree = ""; + }; + C445FA962987D0CF0054D761 /* Sound_Assets */ = { + isa = PBXGroup; + children = ( + C445FA942987D01C0054D761 /* train_horn.mp3 */, + ); + path = Sound_Assets; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -490,6 +517,7 @@ buildActionMask = 2147483647; files = ( C4060DC7297AE73D003FAB80 /* Preview Assets.xcassets in Resources */, + C445FA952987D01C0054D761 /* train_horn.mp3 in Resources */, C4060DC4297AE73D003FAB80 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -538,9 +566,11 @@ C438C81129829EAF00BF3EF9 /* PropertyWrappers.swift in Sources */, C438C807298195E600BF3EF9 /* Model+Extensions.swift in Sources */, C438C7FF2981300500BF3EF9 /* IntentDataProvider.swift in Sources */, + C445FA922987CC8A0054D761 /* Sound.swift in Sources */, C4742B59298411E800D5D950 /* CountdownFormView.swift in Sources */, C4060DC2297AE73B003FAB80 /* ContentView.swift in Sources */, C438C7C12980228B00BF3EF9 /* CountdownScheduler.swift in Sources */, + C445FA8F2987B83B0054D761 /* SoundPlayer.swift in Sources */, C438C7C929803CA000BF3EF9 /* AppDelegate.swift in Sources */, C4060DF5297AE9A7003FAB80 /* TimeInterval+Extensions.swift in Sources */, C4060DF7297AFEF2003FAB80 /* NewCountdownView.swift in Sources */, @@ -549,7 +579,7 @@ C438C7C5298024E900BF3EF9 /* NSManagedContext+Extensions.swift in Sources */, C4060DC0297AE73B003FAB80 /* LeCountdownApp.swift in Sources */, C438C7E02981216300BF3EF9 /* LaunchWidget.intentdefinition in Sources */, - C4742B5729840F6400D5D950 /* Media.swift in Sources */, + C4742B5729840F6400D5D950 /* CoolPic.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -574,9 +604,10 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + C445FA932987CF280054D761 /* Sound.swift in Sources */, C438C7EB2981266F00BF3EF9 /* SingleCountdownView.swift in Sources */, C438C7D62981216200BF3EF9 /* LaunchWidgetBundle.swift in Sources */, - C445FA87298448730054D761 /* Media.swift in Sources */, + C445FA87298448730054D761 /* CoolPic.swift in Sources */, C438C8172982BE9C00BF3EF9 /* Model+Extensions.swift in Sources */, C438C8162982BE1E00BF3EF9 /* LeCountdown.xcdatamodeld in Sources */, C438C8152982BD9000BF3EF9 /* IntentDataProvider.swift in Sources */, @@ -599,7 +630,7 @@ C438C81A2982BFF100BF3EF9 /* NSManagedContext+Extensions.swift in Sources */, C438C8012981327600BF3EF9 /* Persistence.swift in Sources */, C438C7FD29812BF700BF3EF9 /* LaunchWidget.intentdefinition in Sources */, - C445FA86298448720054D761 /* Media.swift in Sources */, + C445FA86298448720054D761 /* CoolPic.swift in Sources */, C438C800298130E900BF3EF9 /* IntentDataProvider.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -1053,10 +1084,11 @@ C4060DCA297AE73D003FAB80 /* LeCountdown.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + C445FA902987C0CF0054D761 /* LeCountdown.0.2.xcdatamodel */, C418A14F298428CB00C22230 /* LeCountdown.0.1.xcdatamodel */, C4060DCB297AE73D003FAB80 /* LeCountdown.xcdatamodel */, ); - currentVersion = C418A14F298428CB00C22230 /* LeCountdown.0.1.xcdatamodel */; + currentVersion = C445FA902987C0CF0054D761 /* LeCountdown.0.2.xcdatamodel */; path = LeCountdown.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/LeCountdown/AppDelegate.swift b/LeCountdown/AppDelegate.swift index e222119..20f1031 100644 --- a/LeCountdown/AppDelegate.swift +++ b/LeCountdown/AppDelegate.swift @@ -22,14 +22,15 @@ class AppDelegate : NSObject, UIApplicationDelegate { extension AppDelegate: UNUserNotificationCenterDelegate { func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse) async { - print("didReceive response") + + AppEnvironment.sun.stopSoundIfNecessary() } func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { print("willPresent notification") - completionHandler([.banner, .sound]) + completionHandler([.banner]) - AppEnvironment.sun.endCountdown(countdownId: notification.request.identifier, cancel: false) + AppEnvironment.sun.notifyUser(countdownId: notification.request.identifier, cancel: false) } diff --git a/LeCountdown/CountdownScheduler.swift b/LeCountdown/CountdownScheduler.swift index b35ddf1..0e651cf 100644 --- a/LeCountdown/CountdownScheduler.swift +++ b/LeCountdown/CountdownScheduler.swift @@ -38,7 +38,9 @@ class CountdownScheduler { content.sound = UNNotificationSound.defaultCritical let trigger = UNTimeIntervalNotificationTrigger(timeInterval: duration, repeats: false) - let request = UNNotificationRequest(identifier: countdown.objectID.uriRepresentation().absoluteString, content: content, trigger: trigger) + let request = UNNotificationRequest(identifier: countdown.objectID.uriRepresentation().absoluteString, + content: content, + trigger: trigger) UNUserNotificationCenter.current().add(request) { error in DispatchQueue.main.async { if let error { @@ -64,6 +66,8 @@ class AppEnvironment : ObservableObject { static let sun: AppEnvironment = AppEnvironment() + var soundPlayer: SoundPlayer? = nil + @UserDefault(Key.dates.rawValue, defaultValue: [:]) static var savedDates: [String : DateInterval] init() { @@ -87,6 +91,11 @@ class AppEnvironment : ObservableObject { } } + 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 { @@ -118,4 +127,26 @@ class AppEnvironment : ObservableObject { } } + // 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() + } + } diff --git a/LeCountdown/Model/LeCountdown.xcdatamodeld/.xccurrentversion b/LeCountdown/Model/LeCountdown.xcdatamodeld/.xccurrentversion index c4c1845..1dcfe8d 100644 --- a/LeCountdown/Model/LeCountdown.xcdatamodeld/.xccurrentversion +++ b/LeCountdown/Model/LeCountdown.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - LeCountdown.0.1.xcdatamodel + LeCountdown.0.2.xcdatamodel diff --git a/LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.2.xcdatamodel/contents b/LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.2.xcdatamodel/contents new file mode 100644 index 0000000..8f439d0 --- /dev/null +++ b/LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.2.xcdatamodel/contents @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/LeCountdown/Model/Model+Extensions.swift b/LeCountdown/Model/Model+Extensions.swift index 57eef20..21e1d13 100644 --- a/LeCountdown/Model/Model+Extensions.swift +++ b/LeCountdown/Model/Model+Extensions.swift @@ -30,6 +30,10 @@ extension Countdown { return CoolPic.allCases[0].rawValue } + 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 new file mode 100644 index 0000000..02999c5 --- /dev/null +++ b/LeCountdown/Sound/Sound.swift @@ -0,0 +1,38 @@ +// +// Sound.swift +// LeCountdown +// +// Created by Laurent Morvillier on 30/01/2023. +// + +import Foundation + +struct SoundFile { + var filename: String + var fileExtension: String + + var url: URL? { + return Bundle.main.url(forResource: self.filename, withExtension: self.fileExtension) + } +} + +// Sound id are stored thus case order should not be changed +enum Sound : Int, CaseIterable, Identifiable { + + var id: Int { return self.rawValue } + + case trainhorn // default + + var localizedString: String { + switch self { + case .trainhorn: return NSLocalizedString("Train horn", comment: "") + } + } + + var soundFile: SoundFile { + switch self { + case .trainhorn: return SoundFile(filename: "train_horn", fileExtension: "mp3") + } + } + +} diff --git a/LeCountdown/Sound/SoundPlayer.swift b/LeCountdown/Sound/SoundPlayer.swift new file mode 100644 index 0000000..6ddd3c5 --- /dev/null +++ b/LeCountdown/Sound/SoundPlayer.swift @@ -0,0 +1,44 @@ +// +// SoundPlayer.swift +// LeCountdown +// +// Created by Laurent Morvillier on 30/01/2023. +// + +import Foundation +import AVFoundation + +enum SoundPlayerError : Error { + case missingResourceError(file: SoundFile) +} + +class SoundPlayer { + + fileprivate var _player: AVAudioPlayer? + + func playSound(soundFile: SoundFile, repeats: Bool) throws { + guard let url = soundFile.url else { + throw SoundPlayerError.missingResourceError(file: soundFile) + } + + _player = try AVAudioPlayer(contentsOf: url) + _player?.prepareToPlay() + let loopCount = repeats ? Int.max : 0 + _player?.numberOfLoops = loopCount + _player?.volume = 1.0 + + do { + try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default, options: [.allowBluetooth, .defaultToSpeaker]) + } catch { + print("audioSession error = \(error)") + } + + _player?.play() + + } + + func stop() { + self._player?.stop() + } + +} diff --git a/LeCountdown/Sound_Assets/train_horn.mp3 b/LeCountdown/Sound_Assets/train_horn.mp3 new file mode 100644 index 0000000..339dd10 Binary files /dev/null and b/LeCountdown/Sound_Assets/train_horn.mp3 differ diff --git a/LeCountdown/Media.swift b/LeCountdown/Utils/CoolPic.swift similarity index 70% rename from LeCountdown/Media.swift rename to LeCountdown/Utils/CoolPic.swift index 9f245c1..42483e0 100644 --- a/LeCountdown/Media.swift +++ b/LeCountdown/Utils/CoolPic.swift @@ -7,15 +7,6 @@ import Foundation -enum Sound : String, CaseIterable, Identifiable { - - var id: String { return self.rawValue } - - case sound1 - case pouet - -} - enum CoolPic : String, CaseIterable, Identifiable { var id: String { return self.rawValue } diff --git a/LeCountdown/Views/CountdownFormView.swift b/LeCountdown/Views/CountdownFormView.swift index d4f309d..d33e88c 100644 --- a/LeCountdown/Views/CountdownFormView.swift +++ b/LeCountdown/Views/CountdownFormView.swift @@ -14,8 +14,9 @@ struct CountdownFormView : View { var minutesBinding: Binding var nameBinding: Binding - var soundBinding: Binding var imageBinding: Binding + var soundBinding: Binding + var repeatsBinding: Binding var textFieldIsFocused: FocusState.Binding @@ -38,11 +39,12 @@ struct CountdownFormView : View { Picker(selection: soundBinding) { ForEach(Sound.allCases) { sound in - Text(sound.rawValue).tag(sound) + Text(sound.localizedString).tag(sound) } } label: { Text("Sound") } + Toggle("Sound repeats", isOn: repeatsBinding) } @@ -77,7 +79,6 @@ struct CountdownFormView_Previews: PreviewProvider { static var previews: some View { CountdownFormView(secondsBinding: .constant(""), minutesBinding: .constant(""), - nameBinding: .constant(""), soundBinding: .constant(.sound1), - imageBinding: .constant(.pic3), textFieldIsFocused: $textFieldIsFocused) + nameBinding: .constant(""), imageBinding: .constant(.pic3), soundBinding: .constant(.trainhorn), repeatsBinding: .constant(true), textFieldIsFocused: $textFieldIsFocused) } } diff --git a/LeCountdown/Views/NewCountdownView.swift b/LeCountdown/Views/NewCountdownView.swift index 993a0dc..4512956 100644 --- a/LeCountdown/Views/NewCountdownView.swift +++ b/LeCountdown/Views/NewCountdownView.swift @@ -36,7 +36,8 @@ struct CountdownEditView : View { @State var minutesString: String = "" @State var nameString: String = "" - @State var sound: Sound = .sound1 + @State var sound: Sound = .trainhorn + @State var soundRepeats: Bool = true @State var image: CoolPic = .pic1 @State var deleteConfirmationShown: Bool = false @@ -59,7 +60,9 @@ struct CountdownEditView : View { CountdownFormView(secondsBinding: $secondsString, minutesBinding: $minutesString, nameBinding: $nameString, - soundBinding: $sound, imageBinding: $image, + imageBinding: $image, + soundBinding: $sound, + repeatsBinding: $soundRepeats, textFieldIsFocused: $textFieldIsFocused) .onAppear { self._onAppear() @@ -145,6 +148,15 @@ struct CountdownEditView : View { if let name = countdown.activity?.name, !name.isEmpty { self.nameString = name } + + if let sound = Sound(rawValue: Int(countdown.sound)) { + self.sound = sound + } + self.soundRepeats = countdown.repeats + + if let image = countdown.image, let coolpic = CoolPic(rawValue: image) { + self.image = coolpic + } } } @@ -180,7 +192,8 @@ struct CountdownEditView : View { } cd.image = self.image.rawValue - cd.sound = self.sound.rawValue + cd.sound = Int16(self.sound.rawValue) + cd.repeats = self.soundRepeats if !self.nameString.isEmpty {