如何从UIViewControllerRepresentable向父SwiftUI视图传递数据?
问题:从UIViewControllerRepresentable向父SwiftUI视图传递数据
我们定义了一个200×200的视图,内部包含视频播放器。但无法预知将要加载的视频比例(16:9、3:4、1:1等),希望在AVPlayer实际显示视频的区域(而非黑边部分)上方叠加视图。比如16:9的视频加载到200×200的AVPlayer视图中,播放器整体是200×200,但实际视频显示区域约为200×112.666。
(注:聚焦于向SwiftUI视图回传数据的具体问题,以下简化示例可能缺失部分背景逻辑)
SwiftUI视图代码
struct VideoWithPossibleVideoBoundedOverlayView: View { @State var player = AVPlayer(url: URL(string: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")!) @State var playerVideoBounds: CGRect = .zero @State var forceUpdateToggle: Bool = false var body: some View { PlayerController(player: $player, playerVideoBounds: $playerVideoBounds, forceUpdateToggle: $forceUpdateToggle) .frame(width: 200, height: 200) .onAppear { player.play() } .onTapGesture { // 手动触发UIViewRepresentable更新的测试代码 forceUpdateToggle.toggle() } } }
UIViewControllerRepresentable代码
struct PlayerController: UIViewControllerRepresentable { typealias UIViewControllerType = AVPlayerViewController @Binding var player: AVPlayer @Binding var playerVideoBounds: CGRect // 希望在这里回传视频边界 @Binding var forceUpdateToggle: Bool func makeCoordinator() -> Coordinator { return Coordinator(self) } func makeUIViewController(context: UIViewControllerRepresentableContext<PlayerController>) -> AVPlayerViewController { let controller = AVPlayerViewController() controller.player = player controller.showsPlaybackControls = false return controller } func updateUIViewController(_ uiViewController: AVPlayerViewController, context: UIViewControllerRepresentableContext<PlayerController>) { if uiViewController.player != player { uiViewController.player = player } print("\nforceUpdateToggle: \(forceUpdateToggle)") print("VideoBounds: \(uiViewController.videoBounds)") // 这里能拿到视频实际显示区域的边界 playerVideoBounds = uiViewController.videoBounds context.coordinator.parent.playerVideoBounds = uiViewController.videoBounds print("playerVideoBounds: \(playerVideoBounds)") print("context.coordinator.parent.playerVideoBounds: \(context.coordinator.parent.playerVideoBounds)") context.coordinator.updateVideoBounds(videoBounds: uiViewController.videoBounds) print("playerVideoBounds: \(playerVideoBounds)") print("context.coordinator.parent.playerVideoBounds: \(context.coordinator.parent.playerVideoBounds)") } class Coordinator: NSObject, AVPlayerViewControllerDelegate { var parent: PlayerController init(_ parent: PlayerController) { self.parent = parent } func updateVideoBounds(videoBounds: CGRect) { parent.playerVideoBounds = videoBounds print("parent.playerVideoBounds: \(parent.playerVideoBounds)") } } }
尝试后的打印结果
forceUpdateToggle: false VideoBounds: (0.0, 43.666666666666664, 200.0, 112.66666666666667) playerVideoBounds: (0.0, 0.0, 0.0, 0.0) context.coordinator.parent.playerVideoBounds: (0.0, 0.0, 0.0, 0.0) parent.playerVideoBounds: (0.0, 0.0, 0.0, 0.0) playerVideoBounds: (0.0, 0.0, 0.0, 0.0) context.coordinator.parent.playerVideoBounds: (0.0, 0.0, 0.0, 0.0)
解决方案
问题核心在于两点:一是updateUIViewController的调用时机早于视频实际渲染完成时间,此时videoBounds还未更新为有效值;二是直接在updateUIViewController中赋值Binding的方式无法实时捕获videoBounds的变化。
修正方案:使用KVO监听视频边界变化
通过KVO监听AVPlayerViewController的videoBounds属性,当视频实际渲染完成后,实时将正确的边界值传递回SwiftUI视图。
修改后的PlayerController代码
struct PlayerController: UIViewControllerRepresentable { typealias UIViewControllerType = AVPlayerViewController @Binding var player: AVPlayer @Binding var playerVideoBounds: CGRect func makeCoordinator() -> Coordinator { Coordinator(self) } func makeUIViewController(context: Context) -> AVPlayerViewController { let controller = AVPlayerViewController() controller.player = player controller.showsPlaybackControls = false controller.delegate = context.coordinator // 为videoBounds添加KVO监听 controller.addObserver(context.coordinator, forKeyPath: #keyPath(AVPlayerViewController.videoBounds), options: [.new], context: nil) return controller } func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) { if uiViewController.player != player { uiViewController.player = player } } func dismantleUIViewController(_ uiViewController: AVPlayerViewController, context: Context) { // 移除KVO监听,避免内存泄漏 uiViewController.removeObserver(context.coordinator, forKeyPath: #keyPath(AVPlayerViewController.videoBounds)) } class Coordinator: NSObject, AVPlayerViewControllerDelegate { var parent: PlayerController init(_ parent: PlayerController) { self.parent = parent } // KVO回调方法,监听videoBounds变化 override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { guard keyPath == #keyPath(AVPlayerViewController.videoBounds), let newBounds = change?[.newKey] as? CGRect, newBounds != .zero else { return } // 必须在主线程更新SwiftUI状态 DispatchQueue.main.async { self.parent.playerVideoBounds = newBounds print("更新后的视频边界:\(newBounds)") } } } }
修改后的SwiftUI视图代码
移除不必要的forceUpdateToggle,KVO会自动监听视频边界变化:
struct VideoWithPossibleVideoBoundedOverlayView: View { @State var player = AVPlayer(url: URL(string: "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4")!) @State var playerVideoBounds: CGRect = .zero var body: some View { ZStack { PlayerController(player: $player, playerVideoBounds: $playerVideoBounds) .frame(width: 200, height: 200) // 叠加在视频实际显示区域的示例视图 if playerVideoBounds != .zero { Rectangle() .fill(.red.opacity(0.3)) .frame(width: playerVideoBounds.width, height: playerVideoBounds.height) .position(x: playerVideoBounds.midX, y: playerVideoBounds.midY) } } .onAppear { player.play() } } }
关键说明
- KVO监听:
videoBounds只有在视频实际渲染完成后才会有正确的值,通过KVO可以实时捕获这个变化,无需依赖updateUIViewController的调用时机。 - 主线程更新:KVO回调可能在后台线程触发,必须切换到主线程更新SwiftUI的
@State/@Binding状态,否则会导致UI更新异常。 - 移除监听:在
dismantleUIViewController中移除KVO监听,避免内存泄漏。 - 移除冗余代码:不需要
forceUpdateToggle手动触发更新,KVO会自动处理边界变化的通知。
内容的提问来源于stack exchange,提问作者chris P
相关产品推荐
相关产品推荐

