AVAudioPlayer delegate causing retain cycle.

It appears that AVAudioPlayer is maintaining a strong reference to my containing class. Here is the essential code. Pay attention to the comments.

class StethRecording: NSObject, ObservableObject, Identifiable {
    let player: AVAudioPlayer?
    let id = UUID()

    @Published var isPlaying = false
    @Published var progress = 0.0

    init(file: AVAudioFile) throws {
        player = try AVAudioPlayer(contentsOf: file.url)
        super.init()
        // I used to assign the player delegate here.
        // If I do that, when I delete this object, it
        // doesn't go away.
        player!.prepareToPlay()
    }
    
    deinit {
        // If this object doesn't go away, I leave data.
        // behind. Something I don't want to do.
        try? deleteAssociatedAudioFile()
    }

    func play() {
        guard let player else { return }
        // So now I have to assign the delegate whenever
        // I start playing.
        player.delegate = self
        isPlaying = true
        player.play()
        startUpdateTimer()
    }

    func stop() {
        guard let player else { return }
        player.stop()
        playbackConcluded()
    }

    // MARK: - Private Methods
    
    private func playbackConcluded() {
        isPlaying = false
        stopUpdateTimer()
        updateProgress()
        player!.reset()
        // I also have to remove the delegate when I
        // stop, for any reason.
        player!.delegate = nil
        player!.prepareToPlay()
    }
}

extension StethRecording: AVAudioPlayerDelegate {
    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
        playbackConcluded()
    }
}

This works, but is this approach really necessary? I would expect the AVAudioPlayer to use a weak reference for the delegate. Or, am I doing something else wrong here?

Answered by Engineer in 810810022

Hello @statemachinejunkie, thank you for your post. The delegate property of AVAudioPlayer has the following declaration:

weak var delegate: (any AVAudioPlayerDelegate)? { get set }

So the reference is weak. Deallocating an instance of StethRecording should automatically release resources allocated with its player, unless the AVAudioFile used to construct the StethRecording instance is being retained somewhere else.

The player isn't really using this AVAudioFile, you could change the initializer of StethRecording to take a URL instead. You can also declare the player as a var instead of a let constant. This allows you to deallocate it (by setting it to nil) without necessarily deallocating the underlying StethRecording instance.

Accepted Answer

Hello @statemachinejunkie, thank you for your post. The delegate property of AVAudioPlayer has the following declaration:

weak var delegate: (any AVAudioPlayerDelegate)? { get set }

So the reference is weak. Deallocating an instance of StethRecording should automatically release resources allocated with its player, unless the AVAudioFile used to construct the StethRecording instance is being retained somewhere else.

The player isn't really using this AVAudioFile, you could change the initializer of StethRecording to take a URL instead. You can also declare the player as a var instead of a let constant. This allows you to deallocate it (by setting it to nil) without necessarily deallocating the underlying StethRecording instance.

AVAudioPlayer delegate causing retain cycle.
 
 
Q