Why `audioPlayerDidFinishPlaying` is still alive after dismiss the current viewController it belongs to

ChuckZHB

I have a modalPresentedVC named PlayerViewController showing the music playing panel, the one includes some play, pause buttons and image cover.

And I implement audioPlayerDidFinishPlaying to let it continue to play next song when current one is complete. I notice that after I dismiss this PlayerViewController viewController, the audio play will go to the next song after the current is done. That is what I want, it is cool!

But I don't understand the things behind this. I think I have dismissed this ViewController, then the function audioPlayerDidFinishPlaying should not be accessed anyway from outside. But it still is called as I put print("I'm alive") in that method proving it.

So is that mean audioPlayerDidFinishPlaying could work outside its current viewController as build-in support? And we don't need any extra effort.

func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
    if flag {
        goNextTrack()
        print("I'm alive")
        // omitted code
        updateTimeLabel()
    } else {
        fatalError("the audio doesn't finished playing successfully.")
    }
}

func goNextTrack() {
    if modelController.playMode == .cycle {
        self.position = (position + 1) % songs.count
        playSong()
    } else if modelController.playMode == .cycleOne {
        playSong()
    } else {
        songs.shuffle()
        playSong()
    }
}

func playSong() {
    let song = songs[position]
    guard let url = song.path else { return }
    AudioPlayer.shared.play(url: url)
    guard let player = AudioPlayer.shared.player else { return }

    // set player.delegate to current VC
    player.delegate = self 
    // omitted code
}
Chip Jarred

In playSong you assign self as the delegate to AudioPlayer, so it retains a reference to your PlayerViewController, so even though you have dismissed it, the AudioPlayer keeps it alive, and calls audioPlayerDidFinishPlaying when it finishes playing the song.

Of course, the next natural question is, "why does the AudioPlayer remain alive?" I'm pretty sure that's because behind the scenes it does the work of playing the song in an @escaping closure in a concurrent task. To do its work that closure has to capture a reference to the player, keeping it alive.

Regarding getting duplicate PlayerViewController instances, that's because the AudioPlayer is a singleton and the PlayerViewController is being used as the AudioPlayer's delegate, so the PlayerViewController is kept alive by the AudioPlayer. Opening the view again spawns a new PlayerViewController, but the old one still exists in the AudioPlayer.

The solution is to refactor so PlayerViewController is no longer the AVAudioPlayerDelegate.

So you move the methods for AVAudioPlayerDelegate conformance from your PlayerViewController to a separate object. In the following code I assume songs is an array of Song, and that modelController is a ModelController, since those definitions aren't in the provided code.

In this case, I think the best candidate for the AVAudioPlayerDelegate seems to be AudioPlayer itself.

Also since we're trying to decouple the AudioPlayer from the PlayerViewController, we still need a way for the AudioPlayer to communicate when a song ends so that it can update it's time label. In preparation for that I declare a PlayerListener protocol (where in this case "listener" refers to the design pattern rather than listener of music).

protocol PlayerListener: class
{
    func receivePlayerMessage(_ message: AudioPlayer.Message)
}

@objc class AudioPlayer: NSObject, AVAudioPlayerDelegate
{
    // Singleton pattern
    static let shared = AudioPlayer()
    var player: AVAudioPlayer? = nil

    var songs: [Song] = []
    var position: Int = 0
    var modelController: ModelController? = nil
    
    enum Message
    {
        case songFinished
        case songStarted
    }
    
    private var listeners: [PlayerListener] = []
    
    private override init() {}
    
    func add(listener: PlayerListener) {
        listeners.append(listener)
    }
    
    func remove(listener: PlayerListener) {
        listeners.removeAll { $0 === listener }
    }
    
    func broadcast(message: Message) {
        listeners.forEach { $0.receivePlayerMessage(message) }
    }
    
    func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool)
    {
        guard !flag else {
            fatalError("the audio doesn't finished playing successfully.")
        }
        
        goNextTrack()
        print("I'm alive")
        broadcast(message: .songFinished)
    }
    
    func goNextTrack()
    {
        guard let playMode = modelController?.playMode else { return }
        
        switch playMode
        {
            case .cycle: position = (position + 1) % songs.count
            case .cycleOne:  break
            default: songs.shuffle()
        }
        
        playSong()
    }
    
    func playSong()
    {
        guard songs.indices.contains(position) else { return }
        
        let song = songs[position]
        guard let url = song.path else { return }
        
        AudioPlayer.shared.play(url: url)
    }
    
    func play(url: URL)
    {
        do
        {
            player = try AVAudioPlayer(contentsOf: url)
            guard let player = player else { return }
            player.delegate = self
            player.prepareToPlay()
            player.play()
            broadcast(message: .songStarted)
        }
        catch { print(error) }
    }
}

Remove AVAudioPlayerDelegate conformance from your PlayerViewController, and add conformance to PlayerListener.

Presumably it still needs to respond to playSong() and maybe goNextTrack() in response to UI elements. Also since the AudioPlayer needs song data, you need to set that whenever your view loads/appears. Because you mention re-using the view controller, let's do that in viewDidAppear(), though I question that approach - in my mind each view (or maybe view hierarchy) should have its own view controller instance. But that's just me.

// in PlayerViewController
    var position: Int
    {
        get { AudioPlayer.shared.position }
        set { AudioPlayer.shared.position = newValue }
    }
    
    func receivePlayerMessage(_ message: AudioPlayer.Message)
    {
        switch message
        {
            case .songStarted: updateTimeLabel()
            default: break
        }
    }

    override func viewDidAppear()
    {
        AudioPlayer.shared.songs = songs
        AudioPlayer.shared.modelController = modelController
        AudioPlayer.shared.add(listener: self)
    }
    
    override func viewDidDisappear() {
        AudioPlayer.shared.remove(listener: self)
    }

    func goNextTrack() {
        AudioPlayer.shared.goNextTrack()
    }

    func playSong() {
        AudioPlayer.shared.playSong()
    }

Now you have one player delegate for the life of the AudioPlayer. When your view controller is dismissed it will not be kept alive (at least not by the AudioPlayer, and thus can be deinitialized and deallocated, so you don't end up with multiple instances of the view controller.

You did mention that you wanted to re-use the view controller, which is why we set AudioPlayer data in viewDidAppear instead of viewDidLoad. I'm not sure that's a good idea, and is a separate question. I'll address it somewhat in comments.

Collected from the Internet

Please contact [email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related

Why child process still alive after parent process was killed in Linux?

Why is QWebEngineUrlRequestInterceptor still alive after app.quit()

Why are the parent's methods still alive after child's destruction

Why are Vulkan command buffers still "alive" after being submitted to the gpu?

Dismiss a modal ViewController after X seconds

Why is it bad practice to have a viewController dismiss itself?

DialogFragment UI still remains after dismiss

UIViewController still in memory after calling dismiss

How to dismiss previous Viewcontroller after presenting new Viewcontroller using Swift?

How to dismiss the current ViewController and go to another View in Swift

Why is my process still alive after an exception thrown in SelectMany whereas an exception in similar rx operator results in an unhandled exception?

A script's background process is still alive after closing the terminal

Local SSH connection still alive after connecting to a VPN

C# reference object still alive after GC.Collect

ViewController sensors still firing after pop in swift

dismiss current view controller AFTER presenting new view controller - swift

How to dismiss viewcontroller in ios?

How to dismiss ViewController in Swift?

Drag down to dismiss a ViewController

How to dismiss viewcontroller in appdelegate?

UIPageViewController not reloading current viewController after foreground app

Is oprofile still alive and well?

Is EJB still alive?

Is the flamingo project still alive?

Is JME still alive

AutomationElement is still alive?

dismiss ViewController and dismiss keyboard not working together

Why is a still 0 after that operation?

.not(this) on jQuery still identifies this(current row selected) why is that?