I have a looping video player that should play or pause based on a given isPlaying
property. I initially created a looping video player (similar implementation to this example) -- now I want the ability to pause it.
The UI initializes the video in the correct playing/paused state, but the UI doesn't update when isPlaying
changes unless the view is unmounted and re-rendered.
I used print
to confirm that isPlaying
is the correct value and updateUIView
invokes play
or pause
.
I'm wondering why the UI doesn't show the updated playing/paused of the video.
isPlaying
is passed into the LoopingVideoPlayer
like so
...
var body: some View {
LoopingVideoPlayer(
fileUrl: localUrl,
resizeMode: .resizeAspectFill,
isPlaying: isPlaying)
}
...
My code for the looping video player:
import SwiftUI
import AVKit
import AVFoundation
struct LoopingVideoPlayer: UIViewRepresentable {
var fileUrl: URL
var resizeMode: AVLayerVideoGravity
var isPlaying: Bool = true
private var loopingPlayerUIView: LoopingPlayerUIView
init(fileUrl: URL, resizeMode: AVLayerVideoGravity, isPlaying: Bool = true) {
self.fileUrl = fileUrl
self.resizeMode = resizeMode
self.isPlaying = isPlaying
self.loopingPlayerUIView = LoopingPlayerUIView(frame: .zero)
}
func updateUIView(_ uiView: UIView, context: UIViewRepresentableContext<LoopingVideoPlayer>) {
print("updateUIView called while isPlaying == \(isPlaying)")
if isPlaying {
loopingPlayerUIView.play()
} else {
loopingPlayerUIView.pause()
}
}
func makeUIView(context: Context) -> UIView {
print("makeUIView called")
loopingPlayerUIView.initPlayer(fileUrl: fileUrl, resizeMode: resizeMode)
return loopingPlayerUIView
}
}
class LoopingPlayerUIView: UIView {
private let playerLayer = AVPlayerLayer()
private let queuePlayer = AVQueuePlayer()
private var playerLooper: AVPlayerLooper?
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override init(frame: CGRect) {
super.init(frame: frame)
playerLayer.player = queuePlayer
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
func initPlayer(fileUrl: URL, resizeMode: AVLayerVideoGravity) {
print("initPlayer method invoked")
let avAsset = AVAsset(url: fileUrl)
let avPlayerItem = AVPlayerItem(asset: avAsset)
playerLayer.videoGravity = resizeMode
layer.addSublayer(playerLayer)
playerLooper = AVPlayerLooper(player: queuePlayer, templateItem: avPlayerItem)
}
func play() {
print("play method invoked")
print("playerLayer reference matches queuePlayer: \(queuePlayer === playerLayer.player)")
queuePlayer.play()
}
func pause() {
print("pause method invoked")
print("playerLayer reference matches queuePlayer: \(queuePlayer === playerLayer.player)")
queuePlayer.pause()
}
}
To update the isPlaying
value you'll need to use a Binding variable. Currently when you pass true or false to your LoopingVideoPlayer
representable you're creating a copy of the value that's being initialized. That means when you update your variable, it won't update LoopingVideoPlayer.
However, a Binding creates a single source of truth.
The new code should look something like this:
struct LoopingVideoPlayer: UIViewRepresentable {
var fileUrl: URL
var resizeMode: AVLayerVideoGravity
@Binding var isPlaying: Bool
private var loopingPlayerUIView: LoopingPlayerUIView
init(fileUrl: URL, resizeMode: AVLayerVideoGravity, isPlaying: Binding<Bool>) {
self.fileUrl = fileUrl
self.resizeMode = resizeMode
self._isPlaying = isPlaying
self.loopingPlayerUIView = LoopingPlayerUIView(frame: .zero)
}
...
Then, in your parent view, the isPlaying
variable should be a State value as shown below:
...
@State var isPlaying: Bool = true
var body: some View {
LoopingVideoPlayer(
fileUrl: localUrl,
resizeMode: .resizeAspectFill,
isPlaying: $isPlaying)
}
...
I'd recommend reading up more on the relationship between State and Binding in SwiftUI here.