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

如何从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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 12:30:59