Fix: AVAudioPlayer Fails to Play Sound in Swift

Fix: AVAudioPlayer Fails to Play Sound in Swift

Written by

in

Quick answer: AVAudioPlayer usually fails to play sound because the player instance is stored in a local variable that ARC deallocates as soon as the function returns. Retain the player as a class property, verify your audio file exists in Target > Build Phases > Copy Bundle Resources, and configure AVAudioSession to use the .playback category.

Why this happens

When AVAudioPlayer fails to produce sound without throwing an explicit crash, the issue usually stems from automatic reference counting (ARC). If you initialize AVAudioPlayer inside a local function or button action using let player = try AVAudioPlayer(...), Swift deallocates the instance the moment the function finishes execution. The player object is destroyed microseconds before it can decode and output the first audio buffer.

Another common cause is the hardware Ring/Silent switch on iOS devices. The default audio session category (.soloAmbient) automatically silences app audio whenever the device is set to silent or vibrate mode. Without explicitly setting the audio session category to .playback, testing on physical hardware often results in silence.

Finally, missing bundle resources and incorrect URL resolutions cause silent early exits. If an audio file is dragged into an Xcode project without checking your application’s target membership, Bundle.main.url(forResource:withExtension:) returns nil. Guard statements exit before initialization occurs.

How to fix it

1. Retain the player instance at the scope level

Never instantiate AVAudioPlayer purely as a local variable inside a function. Store the reference in a property on your controller, coordinator, or dedicated audio manager so that ARC maintains a strong reference while playback is active.

import AVFoundation
import UIKit

final class SoundEffectViewController: UIViewController {
    // Strong reference persists for the lifetime of this class
    private var audioPlayer: AVAudioPlayer?

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    @IBAction func playButtonTapped(_ sender: UIButton) {
        playSound(named: "notification", fileExtension: "mp3")
    }

    private func playSound(named name: String, fileExtension: String) {
        guard let soundURL = Bundle.main.url(forResource: name, withExtension: fileExtension) else {
            print("Failed to find \(name).\(fileExtension) in main bundle.")
            return
        }

        do {
            audioPlayer = try AVAudioPlayer(contentsOf: soundURL)
            audioPlayer?.prepareToPlay()
            audioPlayer?.play()
        } catch {
            print("AVAudioPlayer initialization failed: \(error.localizedDescription)")
        }
    }
}

2. Configure and activate AVAudioSession

To ensure audio plays even when the user has enabled the physical silent switch, configure the shared AVAudioSession with the .playback category before calling play().

Call this configuration during app launch or immediately prior to audio playback:

import AVFoundation

func configureAudioSession() {
    do {
        let session = AVAudioSession.sharedInstance()
        try session.setCategory(.playback, mode: .default, options: [])
        try session.setActive(true)
    } catch {
        print("Failed to set AVAudioSession category: \(error.localizedDescription)")
    }
}

If your app plays non-essential UI feedback sounds and you want to respect the user’s silent switch while still allowing background music to mix, use the .ambient category instead:

try AVAudioSession.sharedInstance().setCategory(.ambient, mode: .default)

3. Verify bundle resources in Xcode

If Bundle.main.url(forResource:withExtension:) evaluates to nil, verify that your audio file is bundled with the compiled binary:

  1. Select your project in the Xcode Project Navigator.
  2. Select your application target under Targets.
  3. Navigate to the Build Phases tab.
  4. Expand Copy Bundle Resources.
  5. If your audio file (for example, beep.wav or alert.mp3) is missing from this list, click the + button and select it.

Also check the File Inspector (right sidebar in Xcode) for the audio file itself to ensure the checkbox under Target Membership is enabled for your current app target.

4. Create an Observable audio manager for SwiftUI

In SwiftUI, views are transient value types that re-render frequently. Managing audio state directly inside a SwiftUI View struct leads to unexpected deallocations. Isolate audio playback inside an ObservableObject or @Observable class:

import SwiftUI
import AVFoundation

@Observable
final class SoundManager {
    static let shared = SoundManager()
    private var player: AVAudioPlayer?

    private init() {
        configureSession()
    }

    private func configureSession() {
        do {
            try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default)
            try AVAudioSession.sharedInstance().setActive(true)
        } catch {
            print("Audio session error: \(error.localizedDescription)")
        }
    }

    func playSound(named resource: String, ext: String = "mp3") {
        guard let url = Bundle.main.url(forResource: resource, withExtension: ext) else {
            print("Audio file \(resource).\(ext) not found.")
            return
        }

        do {
            player = try AVAudioPlayer(contentsOf: url)
            player?.prepareToPlay()
            player?.play()
        } catch {
            print("Playback error: \(error.localizedDescription)")
        }
    }
}

struct AudioPlayerView: View {
    @State private var soundManager = SoundManager.shared

    var body: some View {
        Button("Play Notification") {
            soundManager.playSound(named: "chime")
        }
        .buttonStyle(.borderedProminent)
    }
}

How to prevent it

  • Store AVAudioPlayer references in long-lived instances such as view controllers, coordinators, or singleton managers rather than local function scopes.
  • Always call prepareToPlay() on your player instance ahead of time to preload audio hardware buffers and minimize latency.
  • Specify file extensions explicitly (for example, withExtension: "mp3") and ensure the case matches the disk filename exactly, as iOS devices use case-sensitive filesystems.
  • Confirm target membership for all sound assets in the Xcode File Inspector upon adding them to your workspace.
  • Test audio playback on physical devices with the mute switch toggled on to verify your AVAudioSession configuration.

FAQ

Why does audio play in the Simulator but stay silent on a physical iPhone?

The iOS Simulator routes sound through macOS audio drivers, which ignore the iOS hardware Ring/Silent switch. On a physical device, the default .soloAmbient category respects the silent switch and mutes playback. Set the AVAudioSession category to .playback to bypass the mute switch.

Can I play multiple sounds simultaneously with AVAudioPlayer?

Yes. Each active sound requires its own distinct AVAudioPlayer instance held in memory. To play overlapping sound effects, maintain a collection (such as an array or dictionary) of strong player references and remove completed players using AVAudioPlayerDelegate.

What audio formats work best with AVAudioPlayer?

For short sound effects with zero decompression latency, use uncompressed 16-bit linear PCM in .wav or .caf containers. For longer background music or voice recordings where storage space matters, use compressed .mp3 or .m4a (AAC) files.

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.