You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Coordinator类设置Binding<Bool>后,父结构体对应Binding未同步问题排查

问题排查:Coordinator中修改Binding值未触发预期的didSet

我实现了如下的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 18:15:53