Coordinator类设置Binding<Bool>后,父结构体对应Binding未同步问题排查
我实现了如下的VideoPlayerViewRepresentable结构体(SwiftUI的UIViewControllerRepresentable):
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 } } } } }
该结构体与内部Coordinator类通过Binding关联了didError、videoIsFullyLoaded等Bool类型状态,但当在Coordinator中设置didError为true时,父结构体VideoPlayerViewRepresentable中对应的didError的didSet并未触发,状态未同步,请问问题出在哪里?
问题根源与解决方法
1. @Binding属性本身不支持didSet观察者
你在VideoPlayerViewRepresentable中给@Binding var didError: Bool添加的didSet永远不会触发——因为@Binding不是结构体的存储属性,它是对外部状态(比如父视图的@State/@ObservableObject属性)的投影引用。当通过Binding修改值时,修改的是外部状态源,而非Representable内部的存储,所以内部的didSet不会被调用。
正确做法:
- 将
didSet逻辑移到提供Binding的父视图的状态属性上,比如父视图中:@State var didError = false { didSet { // 这里写你需要的错误处理逻辑 } } - 或者在使用
VideoPlayerViewRepresentable的地方,用.onChange(of: didError)监听变化:VideoPlayerViewRepresentable( shouldPlayVideo: $shouldPlay, didError: $didError, // 其他参数 ) .onChange(of: didError) { newValue in if newValue { // 处理错误逻辑 } }
2. 重复添加观察者的潜在问题
你的代码在configureExistingPlayerViewController和configurePlayerViewController中都给AVPlayer添加了AVPlayer.currentItem.status的观察者,这会导致多次触发状态更新,甚至内存泄漏。
修复方法:
在dismantleUIViewController中移除观察者,清理资源:
static func dismantleUIViewController(_ uiViewController: AVPlayerViewController, coordinator: Coordinator) { uiViewController.player?.removeObserver(coordinator, forKeyPath: #keyPath(AVPlayer.currentItem.status)) coordinator.cancellable?.cancel() // 其他需要清理的逻辑 }
3. 确认线程安全性
你的代码中已经用DispatchQueue.main.async包裹了状态修改,这是正确的——SwiftUI的状态更新必须在主线程执行。如果不小心在后台线程修改Binding值,会导致状态不同步或崩溃,这一点需要持续保持。
内容的提问来源于stack exchange,提问作者DevB1

