I'd like to control an instance of a AVPlayer that is instantiated and assigned to a property in the class VideoPlayer which conforms to UIView. To make sure it's clear what my intent is, I've put together a known pattern when dealing with SwiftUI and UIViewRepresentable wrappers, as follows:
class VideoPlayer: UIView {
private let playerLayer = AVPlayerLayer()
private var previewTimer: Timer?
var previewLength: Double
var player: AVPlayer?
init(frame: CGRect, url: URL, previewLength:Double) {
self.previewLength = previewLength
super.init(frame: frame)
self.player = AVPlayer(url: url)
self.player?.volume = 0
self.player?.play()
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: self.player?.currentItem, queue: .main) { [weak self] _ in
self?.player?.seek(to: CMTime.zero)
self?.player?.play()
}
playerLayer.player = player
playerLayer.videoGravity = .resizeAspectFill
playerLayer.backgroundColor = UIColor.black.cgColor
previewTimer = Timer.scheduledTimer(withTimeInterval: previewLength, repeats: true, block: { (timer) in
self.player?.seek(to: CMTime(seconds: 0, preferredTimescale: CMTimeScale(1)))
})
layer.addSublayer(playerLayer)
}
required init?(coder: NSCoder) {
self.previewLength = 15
super.init(coder: coder)
}
override func layoutSubviews() {
super.layoutSubviews()
playerLayer.frame = bounds
}
}
The wrapper MyWrapper exposes the UIView to SwiftUI, and the wrapper can have public properties for example myState:
struct MyWrapper: UIViewRepresentable {
var videoURL: URL
var previewLength: Double
@Binding var mySate: Bool
func makeUIView(context: Context) -> UIView {
let videoPlayer = VideoPlayer(frame: .zero, url: videoURL, previewLength: previewLength)
return videoPlayer
}
func updateUIView(_ uiView: UIView, context: Context) {
if myState {
// do something
} else {
// do something else
}
}
}
If required to control a property of the instance VideoPlayer, we might be tempted to access properties directly from the UIView object in the method updateUIView. So, let's say that hypothetically we want to access the player property:
func updateUIView(_ uiView: UIView, context: Context) {
if myState {
uiView.player.play()
}
}
This fails with the error Value of type 'UIView' has no member 'player'.
To overcome this created a proxy in the MyWrapper to reference the AVPlayer instance, as follows:
struct MyWrapper: UIViewRepresentable {
var videoURL: URL
var previewLength: Double
@State var player: AVPlayer?
@Binding var play: Bool
func makeUIView(context: Context) -> UIView {
let videoPlayer = VideoPlayer(frame: .zero, url: videoURL, previewLength: previewLength)
DispatchQueue.main.async {
self.player = videoPlayer.player
}
return videoPlayer
}
func updateUIView(_ uiView: UIView, context: Context) {
if play {
self.player?.play()
} else {
self.player?.pause()
self.player?.rate = 0
}
}
}
Obs: We use the DispatchQueue.main to bypass a warning Modifying state during view update, this will cause undefined behaviour that'd be thrown in makeUIView.
This approach works but I haven't found any documentation in Apple confirming that this is the right approach when dealing with SwiftUI and UIViewRepresentable wrappers, as SwiftUI is a black box.
So, for this reason I'd like to know how to access a property in SwiftUI wrapper that conforms to UIViewRepresentable?