8

I'm making an alarm clock app, and I have confirmed that my alarm notifications are being triggered correctly, but the sound does not always play as I expect.

For instance, when the phone is not in silent mode, the notification plays the sound sucessfully (ok, great!). However, when the phone is in silent mode, the sound does not play, even though the app is still running in the background (not so great...).

I understand that silent mode is supposed to silence all notification sounds, BUT I've downloaded other alarm clock apps from the App Store (like Alarmy), and they are somehow able to get their notification sounds to play even if the phone is on silent mode, as long as the app is still running in the background. Only when the app is fully exited will the silent mode take effect.

Does anyone know how to achieve this result? Is there some setting or option that I need to declare either in my code or plist file? I've scoured the internet but haven't found anything for this particular issue...

My code to set the AVAudioSession category:

private func setAudioCategory() {
    do {
        // Enable sound (even while in silent mode) as long as app is in foreground.
        try AVAudioSession.sharedInstance().setCategory(.playback)
    }
    catch {
        print(error.localizedDescription)
    }
}

My code to set a notification:

/// Sets a local user notification for the provided `Alarm` object.
static func set(_ alarm: Alarm) {

    // Configure the notification's content.
    let content = UNMutableNotificationContent()
    content.title = NSString.localizedUserNotificationString(forKey: K.Keys.notificationTitle, arguments: nil)
    content.body = NSString.localizedUserNotificationString(forKey: K.Keys.notificationBody, arguments: nil)

    // Get sound name
    let soundName: String = UserDefaultsManager.getAlarmSound().fileNameFull
        
    // Set sound
    content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: soundName))
    content.categoryIdentifier = "Alarm"

    // Configure the time the notification should occur.
    var date = DateComponents()
    date.hour = alarm.hour
    date.minute = alarm.minute

    // Create the trigger & request
    let trigger = UNCalendarNotificationTrigger(dateMatching: date, repeats: false)
    let request = UNNotificationRequest(identifier: alarm.notifID, content: content, trigger: trigger)

    // Schedule the request with the system.
    let notificationCenter = UNUserNotificationCenter.current()
    notificationCenter.add(request, withCompletionHandler: { error in
        if error != nil {
            // TODO: Show Alert for Error
            return
        }
    })
}
Eric
  • 569
  • 4
  • 21
  • 1
    In fact you are taking the risk of having your application rejected by Apple during review, because having sound ran by your app during silent mode is not allowed by the framework. But if you still want to take the risk, take a look at this: http://andrewmarinov.com/building-an-alarm-app-on-ios/ – Climbatize Aug 09 '21 at 03:06
  • Unfortunately, setting the audio session to `.playAndRecord` and enabling Audio in the .plist file for `Required Background Modes` does not seem to work... (from "Microphone method" in linked article). – Eric Aug 09 '21 at 19:24
  • The sound file length must be not longer, than 30 seconds. – Ramis Aug 10 '21 at 10:47
  • @Ramis Noted. The file already plays correctly when not in silent mode so that is not an issue. – Eric Aug 10 '21 at 19:44
  • Does anyone have any workable solutions? (or @Climbatize can you elaborate on if you were able to get the microphone method to work)? – Eric Aug 10 '21 at 22:08
  • Unfortunately @Eric, once I read your question I was curious so I just conducted a search for you and this article was the best resource I could find, but I personally never tested it as I believed it was not possible (never thought about alarm clock apps) – Climbatize Aug 11 '21 at 02:14
  • 2
    @Climbatize Thanks for your contribution. Check out my answer below if your curious how I solved it. – Eric Aug 11 '21 at 06:05

1 Answers1

9

So I've discovered something.

As the question is stated, it is not currently possible to play sound in a local notification when the phone is on silent mode.

However, great news!

There is actually a different way to achieve the same result; and it's how apps like Alarmy do it.

Note: I (FINALLY) discovered this solution from this wonderful SO answer, but I'll summarize it here for reference.

In short, the local notification will not be playing the sound, but instead, the app will play it (while in the background).

STEPS

  1. You must enable the app to play sound in the background. To do this, navigate to your .plist file and add the String value App plays audio or streams audio/video using AirPlay to the array key Required background modes. (This can also be achieved in your app's Capabilities - it does the same thing).

  2. In your App Delegate, set your AVAudioSession's category to .playBack so sound will still play even when the phone is locked or in the background.

do {
    try AVAudioSession.sharedInstance().setCategory(.playAndRecord)
    try AVAudioSession.sharedInstance().setActive(true)
} catch {
    print(error.localizedDescription)
}
  1. Start your AVAudioPlayer at the time you would like it to play (in my case, at the time of my local notification).
let timeInterval = 60.0 // 60.0 would represent 1 minute from now
let timeOffset = audioPlayer.deviceCurrentTime + timeInverval
audioPlayer.play(atTime: timeOffset) // This is the magic function

// Note: the `timeInterval` must be added to the audio player's 
// `.deviceCurrentTime` to calculate a correct `timeOffset` value.

In conclusion, as the SO answer I linked to above so aptly summarizes:

This does not play silence in the background, which violates Apple's rules. It actually starts the player, but the audio will only start at the right time. I think this is probably how Alarmy implemented their alarm, given that it's not a remote notification that triggers the audio nor is the audio played by a local notification (as its not limited to 30 seconds or silenced by the ringer switch).

Eric
  • 569
  • 4
  • 21
  • 1
    nice catch, I'm glad you finally solved your issue, just be careful with the AppStore submission ;) – Climbatize Aug 12 '21 at 03:06
  • Won't this stop any other audio the user is playing? Say if they're listening to music – Mor Blau Aug 15 '21 at 05:17
  • @MorBlau Of course, isn't that what you want when an alarm goes off? However, there are [other options](https://developer.apple.com/documentation/avfaudio/avaudiosession/categoryoptions) you can configure when setting your app's AVAudioSession category if you want different audio results (such as `.mixWithOthers`, `.duckOthers`, etc.) – Eric Aug 16 '21 at 17:24
  • @Eric really nice solution. Unfortunately the ```play(atTime: )``` method does not play the audio, if the audio length is shorter than the timeOffset. Unless I misunderstand what the method or the ```deviceCurrentTime``` does. Could you comment on that? – Celina Mar 28 '22 at 19:54
  • @Celina Hmmm that shouldn't be the case. If I remember correctly, `deviceCurrentTime` was some unpredictably large `TimeInterval` like 125134243.23, so in order to play the audio player in, say, 5 seconds from now, you'd have to go `audioPlayer.play(atTime: 125134243.23 + 5.0)`. As such, the audio length will always be a lot shorter than the timeOffset, so this wouldn't be the reason your audio did not play. – Eric Mar 29 '22 at 07:06
  • 1
    You can consider `audioPlayer.deviceCurrentTime` to be "now" (even though it's some strangely large Double/TimeInterval), and the `timeOffset` as, say, 5 seconds from now (assuming you set the `timeInterval` constant in my solution to 5.0 instead of 60.0. – Eric Mar 29 '22 at 07:08
  • Yes, you are completely right. I try to schedule multiple timer sounds this way and only one is played out loud. Out of coincidence, it was always the longer sound. – Celina Mar 29 '22 at 16:13
  • Yeah your audio players must be interrupting each other. I would research how multiple audio players interact with one another. – Eric Mar 29 '22 at 22:15
  • @Eric "in my case, at the time of my local notification" can you elaborate it? I mean if your app is in background how can it play audio when local notification received ? And also share some more preview of your audioPlayer initializer? – Rashad May 14 '22 at 19:08
  • @Rashad The sound is not triggered as a result of the local notification being received, I simply predetermine when I want the local notification to be received (say one hour from now), and then set my audio player to play the sound in 1 hour (so that it matches and seems to the user like the sound is playing due to the notification). Hope that clears things up for you. – Eric May 17 '22 at 02:07
  • @Rashad My audio players is initialized just like any other time. Just research how to initialize an AVAudioPlayer if you are unfamiliar. – Eric May 17 '22 at 02:08
  • @Eric Just checking - if I use `audioPlayer.play(atTime:`) for some future date, let's say even a month from now - this will still work? Meaning - by virtue of simply calling this method, the app will remain in a state that will allow the eventual "playing" to occur? I'm just concerned the the OS will throw the app into a suspended state at some point, thereby preventing the play(atTime) to follow through. – cohen72 Aug 14 '22 at 17:53
  • @cohen72 From my understanding and experience, yes. However, to be more sure, maybe you can test it out yourself by scheduling it a few days in the future and see what happens. It seems to always work for me though (although I only schedule a max of 24 hours in advance). – Eric Aug 15 '22 at 02:46
  • Ok. So in short, we must schedule a local notification **and** a sound to play at a specific time ? I need to play a sound, even in background, when I detect a fall or a shock, and when the user is standing still. So I guess that just scheduling a local notification and a sound to play in 30s or immediately will do the trick. This would wake up the app that will play the sound. –  Jan 31 '23 at 14:34
  • 1
    Uhh...I don't think so. The notification is not necessary to keep the app "awake". In fact, I don't know if the scheduled sound keeps the app awake either (not sure how that all works under Apple's hood). This post is specific to the question of "how to play a sound at a predetermined time in the future". You can't just apply this situation to "keep the app awake" so that you can register falls/shocks whenever you want even when the app is in the background. Also, Apple would almost certainly reject your app if you're abusing the scheduled sound player just to keep your app alive. – Eric Feb 01 '23 at 16:55