为什么 `audioPlayerDidFinishPlaying` 在关闭它所属的当前 viewController 后仍然存在
Why `audioPlayerDidFinishPlaying` is still alive after dismiss the current viewController it belongs to
我有一个名为 PlayerViewController
的 modalPresentedVC 显示音乐播放面板,其中包括一些播放、暂停按钮和图像封面。
我实现了audioPlayerDidFinishPlaying
让它在当前歌曲完成后继续播放下一首歌曲。我注意到在我关闭此 PlayerViewController
viewController 后,音频播放将在当前完成后转到下一首歌曲。这就是我想要的,很酷!
但是我不明白这背后的事情。我想我已经驳回了这个 ViewController,那么无论如何都不应该从外部访问函数 audioPlayerDidFinishPlaying
。但它仍然被调用,因为我把 print("I'm alive")
放在证明它的那个方法中。
那么这是否意味着 audioPlayerDidFinishPlaying
可以在其当前 viewController 之外作为内置支持工作?而且我们不需要任何额外的努力。
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
}
在 playSong
中,您将 self
作为 delegate
分配给 AudioPlayer
,因此它保留了对您的 PlayerViewController
的引用,因此即使您有关闭它,AudioPlayer
让它保持活动状态,并在播放完歌曲后调用 audioPlayerDidFinishPlaying
。
当然,下一个自然的问题是,“为什么 AudioPlayer
还活着?”我很确定那是因为它在幕后完成了在并发任务的 @escaping
闭包中播放歌曲的工作。为了完成它的工作,闭包必须捕获对播放器的引用,使其保持活动状态。
关于获取重复的 PlayerViewController
实例,那是因为 AudioPlayer
是单例并且 PlayerViewController
被用作 AudioPlayer
的委托,所以 PlayerViewController
由 AudioPlayer
保持存活。再次打开视图会产生一个新的 PlayerViewController
,但旧的仍然存在于 AudioPlayer
。
解决方案是重构 PlayerViewController
不再是 AVAudioPlayerDelegate
。
因此,您将 AVAudioPlayerDelegate
一致性的方法从 PlayerViewController
移至单独的对象。在下面的代码中,我假设 songs
是 Song
的数组,并且 modelController
是 ModelController
,因为这些定义不在提供的代码中。
在这种情况下,我认为 AVAudioPlayerDelegate
的最佳人选似乎是 AudioPlayer
本身。
此外,由于我们试图将 AudioPlayer
与 PlayerViewController
分离,我们仍然需要一种方法让 AudioPlayer
在歌曲结束时进行通信,以便它可以更新这是时间标签。为此,我声明了一个 PlayerListener
协议(在这种情况下,“听众”指的是设计模式而不是音乐的听众)。
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 { [=10=] === listener }
}
func broadcast(message: Message) {
listeners.forEach { [=10=].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 }
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) }
}
}
从您的 PlayerViewController
中删除 AVAudioPlayerDelegate
一致性,并添加对 PlayerListener
.
的一致性
大概它仍然需要响应 playSong()
并且可能 goNextTrack()
响应 UI 元素。此外,由于 AudioPlayer
需要歌曲数据,因此您需要在每次查看 loads/appears 时设置它。因为你提到重新使用视图控制器,让我们在 viewDidAppear()
中这样做,尽管我质疑这种方法 - 在我看来每个视图(或者可能是视图层次结构)都应该有自己的视图控制器实例。但这就是我。
// 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()
}
现在您在 AudioPlayer
的生命周期内拥有一名球员代表。当您的视图控制器被关闭时,它不会保持活动状态(至少不会被 AudioPlayer
保持活动状态,因此可以取消初始化和解除分配,因此您不会得到视图控制器的多个实例。
您确实提到您想要重新使用视图控制器,这就是我们在 viewDidAppear
中设置 AudioPlayer
数据而不是 viewDidLoad
的原因。我不确定这是个好主意,这是一个单独的问题。我会在评论中稍微说明一下。
更新我的代码,只为音频播放器提供一次委托。并添加 AudioPlayer API 代码。
好吧,我在这里真正挣扎的是 PlayerViewController
是一个 modalPresentedVC,它可以从不同的来源打开。在我的例子中,它可以从 tableView 行点击或 miniPlayer 容器(它是 tabBar 顶部的子视图控制器,也可以在整个应用程序页面上看到)打开。
但是因为有PlayerViewController
的retain reference,所以即使是dismissing,它仍然活着。那么我怎么能从 miniPlayer 容器中打开这个相同的 modalPresentedVC,我的意思是当我点击这个 miniPlayer 容器时,这个相同的 PlayerViewController
应该再次出现在前面。
我尝试使用下面的代码再次打开这个 VC,但似乎我重新创建了一个 viewController 实例,然后我现在得到两个 viewController 实例。我怎样才能访问现有的?
let storyboard = UIStoryboard(name: "Main", bundle: nil)
guard let vc = storyboard.instantiateViewController(identifier: "Player") as? PlayerViewController else { return }
present(vc, animated: true)
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 only once
if player.delegate == nil {
player.delegate = self
}
// omitted code
}
我的音频播放器API
class AudioPlayer {
// Singleton pattern
static let shared = AudioPlayer()
var player: AVAudioPlayer?
private init() {}
func play(url: URL) {
do {
player = try AVAudioPlayer(contentsOf: url)
guard let player = player else { return }
player.prepareToPlay()
player.play()
} catch {
print(error)
}
}
}
我有一个名为 PlayerViewController
的 modalPresentedVC 显示音乐播放面板,其中包括一些播放、暂停按钮和图像封面。
我实现了audioPlayerDidFinishPlaying
让它在当前歌曲完成后继续播放下一首歌曲。我注意到在我关闭此 PlayerViewController
viewController 后,音频播放将在当前完成后转到下一首歌曲。这就是我想要的,很酷!
但是我不明白这背后的事情。我想我已经驳回了这个 ViewController,那么无论如何都不应该从外部访问函数 audioPlayerDidFinishPlaying
。但它仍然被调用,因为我把 print("I'm alive")
放在证明它的那个方法中。
那么这是否意味着 audioPlayerDidFinishPlaying
可以在其当前 viewController 之外作为内置支持工作?而且我们不需要任何额外的努力。
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
}
在 playSong
中,您将 self
作为 delegate
分配给 AudioPlayer
,因此它保留了对您的 PlayerViewController
的引用,因此即使您有关闭它,AudioPlayer
让它保持活动状态,并在播放完歌曲后调用 audioPlayerDidFinishPlaying
。
当然,下一个自然的问题是,“为什么 AudioPlayer
还活着?”我很确定那是因为它在幕后完成了在并发任务的 @escaping
闭包中播放歌曲的工作。为了完成它的工作,闭包必须捕获对播放器的引用,使其保持活动状态。
关于获取重复的 PlayerViewController
实例,那是因为 AudioPlayer
是单例并且 PlayerViewController
被用作 AudioPlayer
的委托,所以 PlayerViewController
由 AudioPlayer
保持存活。再次打开视图会产生一个新的 PlayerViewController
,但旧的仍然存在于 AudioPlayer
。
解决方案是重构 PlayerViewController
不再是 AVAudioPlayerDelegate
。
因此,您将 AVAudioPlayerDelegate
一致性的方法从 PlayerViewController
移至单独的对象。在下面的代码中,我假设 songs
是 Song
的数组,并且 modelController
是 ModelController
,因为这些定义不在提供的代码中。
在这种情况下,我认为 AVAudioPlayerDelegate
的最佳人选似乎是 AudioPlayer
本身。
此外,由于我们试图将 AudioPlayer
与 PlayerViewController
分离,我们仍然需要一种方法让 AudioPlayer
在歌曲结束时进行通信,以便它可以更新这是时间标签。为此,我声明了一个 PlayerListener
协议(在这种情况下,“听众”指的是设计模式而不是音乐的听众)。
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 { [=10=] === listener }
}
func broadcast(message: Message) {
listeners.forEach { [=10=].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 }
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) }
}
}
从您的 PlayerViewController
中删除 AVAudioPlayerDelegate
一致性,并添加对 PlayerListener
.
大概它仍然需要响应 playSong()
并且可能 goNextTrack()
响应 UI 元素。此外,由于 AudioPlayer
需要歌曲数据,因此您需要在每次查看 loads/appears 时设置它。因为你提到重新使用视图控制器,让我们在 viewDidAppear()
中这样做,尽管我质疑这种方法 - 在我看来每个视图(或者可能是视图层次结构)都应该有自己的视图控制器实例。但这就是我。
// 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()
}
现在您在 AudioPlayer
的生命周期内拥有一名球员代表。当您的视图控制器被关闭时,它不会保持活动状态(至少不会被 AudioPlayer
保持活动状态,因此可以取消初始化和解除分配,因此您不会得到视图控制器的多个实例。
您确实提到您想要重新使用视图控制器,这就是我们在 viewDidAppear
中设置 AudioPlayer
数据而不是 viewDidLoad
的原因。我不确定这是个好主意,这是一个单独的问题。我会在评论中稍微说明一下。
更新我的代码,只为音频播放器提供一次委托。并添加 AudioPlayer API 代码。
好吧,我在这里真正挣扎的是 PlayerViewController
是一个 modalPresentedVC,它可以从不同的来源打开。在我的例子中,它可以从 tableView 行点击或 miniPlayer 容器(它是 tabBar 顶部的子视图控制器,也可以在整个应用程序页面上看到)打开。
但是因为有PlayerViewController
的retain reference,所以即使是dismissing,它仍然活着。那么我怎么能从 miniPlayer 容器中打开这个相同的 modalPresentedVC,我的意思是当我点击这个 miniPlayer 容器时,这个相同的 PlayerViewController
应该再次出现在前面。
我尝试使用下面的代码再次打开这个 VC,但似乎我重新创建了一个 viewController 实例,然后我现在得到两个 viewController 实例。我怎样才能访问现有的?
let storyboard = UIStoryboard(name: "Main", bundle: nil)
guard let vc = storyboard.instantiateViewController(identifier: "Player") as? PlayerViewController else { return }
present(vc, animated: true)
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 only once
if player.delegate == nil {
player.delegate = self
}
// omitted code
}
我的音频播放器API
class AudioPlayer {
// Singleton pattern
static let shared = AudioPlayer()
var player: AVAudioPlayer?
private init() {}
func play(url: URL) {
do {
player = try AVAudioPlayer(contentsOf: url)
guard let player = player else { return }
player.prepareToPlay()
player.play()
} catch {
print(error)
}
}
}