Today, I encountered an issue that VideoPlayer turned to blank when iPhone screen was rotated.
The VideoPlayer was in a modal view that created with fullScreenCover
. The sample code was like this:
import SwiftUI
import AVKit
struct TopView: View {
private let player = AVQueuePlayer(playerItem: nil)
var body: some View {
VideoPlayer(player: player)
.onAppear {
if let videoURL = Bundle.main.url(forResource: "sample", withExtension: "mov") {
let playerItem = AVPlayerItem(url: videoURL)
player.replaceCurrentItem(with: playerItem)
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
if player.status == .readyToPlay {
player.play()
if player.timeControlStatus == .playing {
timer.invalidate()
}
}
}
}
}
}
}
UIDevice.orientationDidChangeNotification
Using Publisher for At first, I thought I should recreate the player when the screen was rotated.
import SwiftUI
import AVKit
struct TopView: View {
@State private var playerItem:AVPlayerItem?
private let player = AVQueuePlayer(playerItem: nil)
private let publisher = NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)
var body: some View {
if playerItem != nil {
VideoPlayer(player: player)
.onReceive(publisher, perform: { _ in
self.player.pause()
self.playerItem = nil
})
} else {
ProgressView()
.onAppear(perform: setPlayer)
}
}
private func setPlayer() {
if let videoURL = Bundle.main.url(forResource: "sample", withExtension: "mov") {
self.playerItem = AVPlayerItem(url: videoURL)
player.replaceCurrentItem(with: playerItem)
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
if player.status == .readyToPlay {
player.play()
if player.timeControlStatus == .playing {
timer.invalidate()
}
}
}
}
}
}
However, the new code didn't work. I added more debug point and finally found that TopView was recreated when the screen was rotated. Since the view was recreated, the player
in onReceive
was newly created, it couldn't stop the playing of the previously played item.
The issue was because, unlike other structs and objects, which could automatically released when container view was released. AVPlayer
hold its owned reference when playing. This behavior caused the SwiftUI view was not released properly.
Use Binding from parent view
The solution was easy. Since I wanted the player to be constant, I should set it up in the parent view.
import SwiftUI
import AVKit
struct TopView: View {
@Binding var player:AVQueuePlayer
var body: some View {
VideoPlayer(player: player)
.onAppear {
if let videoURL = Bundle.main.url(forResource: "sample", withExtension: "mov") {
let playerItem = AVPlayerItem(url: videoURL)
player.replaceCurrentItem(with: playerItem)
Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { timer in
if player.status == .readyToPlay {
player.play()
if player.timeControlStatus == .playing {
timer.invalidate()
}
}
}
}
}
}
}
Now everything worked fine.
Final Thoughts
Some objects hold their own references as strong. Those objects may keep view from release and cause bugs. We should using Binding to create those objects in a higher view which is not recreated. Then the bugs are fixed.