When Binding<Bool> set in coordinator class, the equivalent binding in parent struct is not being set

36 Views Asked by At

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.

1

There are 1 best solutions below

0
DevB1 On

Figured this out - the didSet will not trigger as the binding itself is not changing, the wrappedValue of that binding is.