Play sound / cancel / repeat

release
Laurent 3 years ago
parent a500ecb6cb
commit 3f60675775
  1. 50
      LeCountdown.xcodeproj/project.pbxproj
  2. 7
      LeCountdown/AppDelegate.swift
  3. 33
      LeCountdown/CountdownScheduler.swift
  4. 2
      LeCountdown/Model/LeCountdown.xcdatamodeld/.xccurrentversion
  5. 21
      LeCountdown/Model/LeCountdown.xcdatamodeld/LeCountdown.0.2.xcdatamodel/contents
  6. 4
      LeCountdown/Model/Model+Extensions.swift
  7. 38
      LeCountdown/Sound/Sound.swift
  8. 44
      LeCountdown/Sound/SoundPlayer.swift
  9. BIN
      LeCountdown/Sound_Assets/train_horn.mp3
  10. 9
      LeCountdown/Utils/CoolPic.swift
  11. 9
      LeCountdown/Views/CountdownFormView.swift
  12. 19
      LeCountdown/Views/NewCountdownView.swift

@ -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 = "<group>"; };
C438C80E29828B8600BF3EF9 /* RecordsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordsView.swift; sourceTree = "<group>"; };
C438C81029829EAF00BF3EF9 /* PropertyWrappers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PropertyWrappers.swift; sourceTree = "<group>"; };
C4742B5629840F6400D5D950 /* Media.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Media.swift; sourceTree = "<group>"; };
C445FA8E2987B83B0054D761 /* SoundPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoundPlayer.swift; sourceTree = "<group>"; };
C445FA902987C0CF0054D761 /* LeCountdown.0.2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = LeCountdown.0.2.xcdatamodel; sourceTree = "<group>"; };
C445FA912987CC8A0054D761 /* Sound.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sound.swift; sourceTree = "<group>"; };
C445FA942987D01C0054D761 /* train_horn.mp3 */ = {isa = PBXFileReference; lastKnownFileType = audio.mp3; path = train_horn.mp3; sourceTree = "<group>"; };
C4742B5629840F6400D5D950 /* CoolPic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoolPic.swift; sourceTree = "<group>"; };
C4742B58298411E800D5D950 /* CountdownFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountdownFormView.swift; sourceTree = "<group>"; };
C4742B5A298414B000D5D950 /* ImageSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageSelectionView.swift; sourceTree = "<group>"; };
C4742B5E2984205000D5D950 /* ViewModifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModifiers.swift; sourceTree = "<group>"; };
@ -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 = "<group>";
};
C445FA8D2987B82E0054D761 /* Sound */ = {
isa = PBXGroup;
children = (
C445FA912987CC8A0054D761 /* Sound.swift */,
C445FA8E2987B83B0054D761 /* SoundPlayer.swift */,
);
path = Sound;
sourceTree = "<group>";
};
C445FA962987D0CF0054D761 /* Sound_Assets */ = {
isa = PBXGroup;
children = (
C445FA942987D01C0054D761 /* train_horn.mp3 */,
);
path = Sound_Assets;
sourceTree = "<group>";
};
/* 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 = "<group>";
versionGroupType = wrapper.xcdatamodel;

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

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

@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>LeCountdown.0.1.xcdatamodel</string>
<string>LeCountdown.0.2.xcdatamodel</string>
</dict>
</plist>

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22A400" minimumToolsVersion="Automatic" sourceLanguage="Swift" usedWithCloudKit="YES" userDefinedModelVersionIdentifier="">
<entity name="Activity" representedClassName="Activity" syncable="YES" codeGenerationType="class">
<attribute name="name" attributeType="String" defaultValueString=""/>
<relationship name="countdowns" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Countdown" inverseName="activity" inverseEntity="Countdown"/>
<relationship name="records" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Record" inverseName="activity" inverseEntity="Record"/>
</entity>
<entity name="Countdown" representedClassName="Countdown" syncable="YES" codeGenerationType="class">
<attribute name="duration" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="image" optional="YES" attributeType="String"/>
<attribute name="order" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="repeats" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
<attribute name="sound" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="activity" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Activity" inverseName="countdowns" inverseEntity="Activity"/>
</entity>
<entity name="Record" representedClassName="Record" syncable="YES" codeGenerationType="class">
<attribute name="end" attributeType="Date" defaultDateTimeInterval="696425400" usesScalarValueType="NO"/>
<attribute name="start" attributeType="Date" defaultDateTimeInterval="696425400" usesScalarValueType="NO"/>
<relationship name="activity" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Activity" inverseName="records" inverseEntity="Activity"/>
</entity>
</model>

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

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

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

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

@ -14,8 +14,9 @@ struct CountdownFormView : View {
var minutesBinding: Binding<String>
var nameBinding: Binding<String>
var soundBinding: Binding<Sound>
var imageBinding: Binding<CoolPic>
var soundBinding: Binding<Sound>
var repeatsBinding: Binding<Bool>
var textFieldIsFocused: FocusState<Bool>.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)
}
}

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

Loading…
Cancel
Save