I have the following VideoPlayerViewRepresentable struct:
enum PlayerItemSource {
case url(URL)
case avPlayerItem(AVPlayerItem)
}
struct VideoPlayerViewRepresentable: UIViewControllerRepresentable {
@EnvironmentObject private var videoPlayerEnvironmentObject: VideoPlayerEnvironmentObject
@Binding var shouldPlayVideo: Bool
@Binding var didError: Bool
@Binding var didFinishPlayingVideo: Bool
@Binding var videoIsFullyLoaded: Bool
let videoURL: URL
let playerItemSource: PlayerItemSource?
// ... more properties
private let playerItemPublisher = NotificationCenter.default.publisher(for: .AVPlayerItemDidPlayToEndTime)
static func dismantleUIViewController(_ uiViewController: AVPlayerViewController, coordinator: Coordinator) {
// ... dismantle
}
func makeUIViewController(context: Context) -> AVPlayerViewController {
var playerItem: AVPlayerItem?
// ... config logic
return playerViewController
}
private func configureExistingPlayerViewController(existingPlayerViewController: AVPlayerViewController, existingPlayer: AVPlayer, context: Context) {
existingPlayerViewController.player = existingPlayer
existingPlayerViewController.player?.currentItem?.seek(to: .zero, completionHandler: nil)
existingPlayerViewController.player?.addObserver(context.coordinator, forKeyPath: #keyPath(AVPlayer.currentItem.status), options: [.new, .old], context: nil)
}
private func configureThumbnail(videoURL: URL, playerViewController: AVPlayerViewController) {
let expectedImageIdentifier = "\(videoURL)-\(videoURL)_image"
if let cachedImage = imageDownloader.imageCache?.image(withIdentifier: expectedImageIdentifier) {
DispatchQueue.main.async {
let thumbnailImageView = UIImageView(image: cachedImage)
thumbnailImageView.contentMode = .scaleAspectFill
thumbnailImageView.frame = playerViewController.view.bounds
thumbnailImageView.tag = 1
playerViewController.view.addSubview(thumbnailImageView)
playerViewController.view.sendSubviewToBack(thumbnailImageView)
videoIsFullyLoaded = true
}
}
}
private func setupContext(_ context: Context, with playerViewController: AVPlayerViewController) {
context.coordinator.cancellable = playerItemPublisher
.sink { notification in
if let playerItem: AVPlayerItem = notification.object as? AVPlayerItem {
DispatchQueue.main.async {
if shouldPlayVideo {
didFinishPlayingVideo = true
playerItem.seek(to: CMTime.zero, completionHandler: nil)
playerViewController.player?.play()
}
}
}
}
}
private func configurePlayerViewController(playerViewController: AVPlayerViewController, player: AVPlayer, context: Context) {
playerViewController.delegate = context.coordinator
context.coordinator.isFullScreenGallery = isFullScreenGallery
player.addObserver(context.coordinator, forKeyPath: #keyPath(AVPlayer.currentItem.status), options: [.new, .old], context: nil)
}
func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) {
if let player = uiViewController.player {
if shouldPlayVideo {
player.play()
} else {
player.pause()
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(didError: $didError, videoIsFullyLoaded: $videoIsFullyLoaded, videoURL: videoURL)
}
final class Coordinator: NSObject, AVPlayerViewControllerDelegate {
@Binding private var didError: Bool
@Binding private var videoIsFullyLoaded: Bool
private var isVideoReadyToPlay: Bool = false
var cancellable: AnyCancellable?
var isFullScreenGallery: Bool = false
var videoURL: URL
var timeObserverToken: Any?
init(didError: Binding<Bool>, videoIsFullyLoaded: Binding<Bool>, videoURL: URL) {
self._didError = didError
self._videoIsFullyLoaded = videoIsFullyLoaded
self.videoURL = videoURL
}
func playerViewController(_ playerViewController: AVPlayerViewController, willBeginFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
playerViewController.showsPlaybackControls = true
}
func playerViewController(_ playerViewController: AVPlayerViewController, willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator) {
coordinator.animate(alongsideTransition: nil) { _ in
playerViewController.player?.play()
}
}
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
if keyPath == #keyPath(AVPlayer.currentItem.status) {
let status: AVPlayerItem.Status
if let currentStatus = (change?[NSKeyValueChangeKey.newKey] as? NSNumber)?.intValue {
status = AVPlayerItem.Status(rawValue: currentStatus) ?? .unknown
} else {
status = .unknown
}
if status == .failed {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.didError = true
VideoPlayerStore.sharedInstance.setPlayerURLError(videoURL)
}
} else if status == .readyToPlay {
isVideoReadyToPlay = true
videoIsFullyLoaded = true
}
}
}
}
}
We have a couple of bindings in here which should be connected between the coordinator struct and the VideoPlayerViewRepresentable class. However, when for example didError is set in the coordinator, the changes are not being triggered on the VideoPlayerViewRepresentable struct as I would expect them to. For example, if I add a didSet to both the didError Bool on the VideoPlayerViewRepresentable and the equivalent one in the coordinator, the coordinator one gets set but the VideoPlayerViewRepresentable does not. Wondering what I am missing here.
Figured this out - the didSet will not trigger as the binding itself is not changing, the wrappedValue of that binding is.