Fix: Critical Alert Sounds Only Play One at a Time in iOS

Fix: Critical Alert Sounds Only Play One at a Time in iOS

Written by

in

Quick answer: This is expected. Notification sounds, including .criticalSoundNamed, are played by the system notification service, not by your app’s audio session, so iOS plays one alert sound at a time and a second one interrupts or replaces the first. If you need overlapping audio, play it yourself with AVAudioPlayer or AVAudioEngine while the app is running, and use the notification only as the wake-up signal.

Why this happens

UNNotificationSound is not an audio player. It is a piece of metadata attached to a UNNotificationContent object, and the actual playback is performed by the system notification service after the notification is delivered. That service is shared by every app on the device and is not connected to your AVAudioSession. It has no mixing graph, no notion of two simultaneous channels, and no API for starting a second sound while the first is still ringing.

That is the whole difference from your previous implementation. Two AVAudioPlayer instances live inside your process, each with its own buffer and its own volume, and the audio session mixes them. Two critical notifications are two requests to a system service that plays alert tones one after another. When they fire close together, one sound wins and the other is superseded or cut short.

UNUserNotificationCenter being a singleton is a red herring. Even with separate centers (which you cannot create), the sound would still be rendered by the same system component. Apple does not expose an API for concurrent notification sounds, so there is nothing to configure or toggle.

A second, quieter problem is timing. UNTimeIntervalNotificationTrigger(timeInterval: 0.1, repeats: false) is a request, not a guarantee. Notification delivery is scheduled by the system and can be delayed or coalesced, especially when several requests land in the same window. Two alarms you expect to be 100 ms apart may be delivered far enough apart to look sequential, or close enough that one replaces the other.

How to fix it

1. Decide whether you actually need two sounds

If both alarms mean “something needs attention now”, one sound is the correct design. Collapse the two states into a single notification whose userInfo describes which alarms are active, and let the user see the detail when they open the app.

let content = UNMutableNotificationContent()
content.title = "Motion alert"
content.body = activeAlarms.count > 1
    ? "Two alerts active: \(activeAlarms.joined(separator: ", "))"
    : "Alert: \(activeAlarms[0])"
content.sound = .criticalSoundNamed(
    UNNotificationSoundName(rawValue: "combined_alert.wav"),
    withAudioVolume: alertVolume
)
content.interruptionLevel = .critical
content.userInfo = ["active_alarms": activeAlarms]

You can also record a single audio file that contains both tones layered, and use it as the notification sound. The system plays one file, but the listener hears both.

2. Play the audio yourself while the app is in the foreground

When your app is active, you are not limited to notification sounds. Keep using AVAudioPlayer (or AVAudioEngine if you need real mixing and effects) and drive the alarms from a timer inside the app. This is the only supported way to get genuinely overlapping audio.

import AVFoundation

final class AlarmAudio {
    private var players: [AVAudioPlayer] = []

    func activateSession() throws {
        let session = AVAudioSession.sharedInstance()
        try session.setCategory(.playback, mode: .default, options: [.mixWithOthers])
        try session.setActive(true)
    }

    func play(_ name: String, volume: Float) throws {
        guard let url = Bundle.main.url(forResource: name, withExtension: "wav") else { return }
        let player = try AVAudioPlayer(contentsOf: url)
        player.numberOfLoops = -1
        player.volume = volume
        player.play()
        players.append(player)   // keep a strong reference or playback stops
    }

    func stopAll() {
        players.forEach { $0.stop() }
        players.removeAll()
    }
}

Use Timer or DispatchSourceTimer for the in-app schedule instead of notification triggers, so the two alarms stay in sync with each other rather than with the notification service.

3. Use the notification as a wake-up, then play the real audio

For the locked-device case, accept that the notification sound is a single alert tone and treat it as a prompt. When the user taps it and your app launches, read userInfo and start the correct alarm audio with the code above. This gives you one system sound at delivery time and full overlapping audio once the app is in front of the user.

func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse
) async {
    let alarms = response.notification.request.content.userInfo["active_alarms"] as? [String] ?? []
    try? alarmAudio.activateSession()
    for alarm in alarms {
        try? alarmAudio.play(alarm, volume: 0.9)
    }
}

4. Check the entitlement and the sound file before blaming mixing

Two things silently degrade critical alerts into ordinary ones:

  • The Critical Alerts entitlement must be granted by Apple and present in the provisioning profile. Without it, interruptionLevel = .critical is ignored and the notification behaves like a normal alert, which is muted by the ring/silent switch and by Focus.
  • Custom notification sounds must live in the app bundle or in Library/Sounds and must be shorter than 30 seconds. A longer file is replaced by the default sound, which can look like “my second sound never played”.

Also confirm the user has not disabled Critical Alerts for your app in Settings; that toggle is per app and overrides your content.

5. Do not try to keep the app alive with silent audio

A common workaround is to play an inaudible loop in the background so AVAudioPlayer stays available for the alarms. That requires the Audio background mode and is exactly the pattern App Review rejects, because it uses background audio for something other than audible playback. It also drains the battery and still fails once iOS terminates the app.

How to prevent it

  • Treat notification sounds as one-shot system alerts, never as a mixer.
  • Model alarm state as data (userInfo) and derive the sound from it, rather than scheduling one notification per alarm.
  • Keep custom notification sounds short, bundled, and tested on a locked device with the ring/silent switch off and on.
  • Verify the Critical Alerts entitlement in the built .entitlements file, not just in the target settings.
  • Test with the app backgrounded, force-quit, and foregrounded — the three cases behave differently.

FAQ

Can two critical alert sounds ever play at the same time?

No. Notification sound playback is handled by the system notification service, and Apple provides no API for concurrent notification sounds. A second alert will interrupt or replace the first.

Is UNUserNotificationCenter being a singleton the cause?

No. The singleton only means you share one center object inside your app. The limitation lives in the system service that renders notification sounds, which is outside your process entirely.

Can I make a notification sound longer than 30 seconds?

Not reliably. Custom notification sounds must be under 30 seconds and present in the app bundle or Library/Sounds; longer files fall back to the default sound. For longer audio, play it from the app with AVAudioPlayer after the user opens the notification.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.