SwiftUI中集成AVPlayerViewController与Google IMA SDK全屏过渡广告适配的官方安全方案咨询
你遇到的这个问题真的是SwiftUI结合AVPlayerViewController和Google IMA SDK时的典型痛点——iOS系统在处理AVPlayerViewController全屏时会偷偷用AVFullScreenViewController把它包起来,这时候IMA广告的父层级没跟上,UIKit直接就抛异常了,我之前帮好几个开发者排查过类似的问题,下面给你几个经过验证的官方安全方案:
方案一:监听系统全屏通知,动态更新IMA的父控制器与容器视图
iOS系统提供了专门的通知来监听AVPlayerViewController的全屏状态变化,我们可以利用这些通知,在进入/退出全屏时手动调整IMA广告的层级和父控制器,完美匹配系统的包装逻辑:
- 添加全屏状态监听
在你封装AVPlayerViewController的UIViewControllerRepresentable实现里,注册AVPlayerViewController.didEnterFullScreenNotification和AVPlayerViewController.didExitFullScreenNotification通知:
struct PlayerContainer: UIViewControllerRepresentable { @Binding var player: AVPlayer? func makeUIViewController(context: Context) -> AVPlayerViewController { let playerVC = AVPlayerViewController() playerVC.player = player // 初始化IMA广告容器(初始用playerVC作为父控制器) let adDisplayContainer = IMAAdDisplayContainer(adContainer: playerVC.view, viewController: playerVC) context.coordinator.adDisplayContainer = adDisplayContainer // 注册全屏通知 NotificationCenter.default.addObserver( context.coordinator, selector: #selector(context.coordinator.handleEnterFullScreen), name: AVPlayerViewController.didEnterFullScreenNotification, object: playerVC ) NotificationCenter.default.addObserver( context.coordinator, selector: #selector(context.coordinator.handleExitFullScreen), name: AVPlayerViewController.didExitFullScreenNotification, object: playerVC ) // 初始化IMA广告加载器 let adLoader = IMAAdLoader(settings: nil) adLoader.delegate = context.coordinator context.coordinator.adLoader = adLoader return playerVC } func updateUIViewController(_ uiViewController: AVPlayerViewController, context: Context) { uiViewController.player = player } func makeCoordinator() -> Coordinator { Coordinator() } class Coordinator: NSObject, IMAAdLoaderDelegate { var adDisplayContainer: IMAAdDisplayContainer? var adViewController: IMAAdViewController? var adLoader: IMAAdLoader? @objc func handleEnterFullScreen(_ notification: Notification) { guard let playerVC = notification.object as? AVPlayerViewController, let fullScreenParent = playerVC.parentViewController as? AVFullScreenViewController else { return } // 关键:更新IMA广告的父控制器为系统的全屏容器 adDisplayContainer?.viewController = fullScreenParent adViewController?.parentViewController = fullScreenParent // 把广告视图移到全屏容器的视图层级中 if let adView = adViewController?.view { fullScreenParent.view.addSubview(adView) adView.frame = playerVC.view.frame // 匹配原播放器视图位置 } } @objc func handleExitFullScreen(_ notification: Notification) { guard let playerVC = notification.object as? AVPlayerViewController else { return } // 退出全屏后恢复原父控制器和视图层级 adDisplayContainer?.viewController = playerVC adViewController?.parentViewController = playerVC if let adView = adViewController?.view { playerVC.view.addSubview(adView) adView.frame = playerVC.view.frame } } // IMA广告加载成功回调,保存adViewController func adLoader(_ adLoader: IMAAdLoader, didReceive adResponse: IMAAdResponse) { guard let adDisplayContainer = adDisplayContainer else { return } adViewController = IMAAdViewController(adContainer: adDisplayContainer) // 后续广告展示逻辑 } // 其他IMAAdLoaderDelegate回调实现 func adLoader(_ adLoader: IMAAdLoader, didFailWithError error: IMAAdLoadingError) { print("广告加载失败:\(error.localizedDescription)") } } }
方案二:自定义全屏过渡,绕过系统的AVFullScreenViewController
如果不想跟系统的全屏容器打交道,完全可以自己控制AVPlayerViewController的全屏逻辑,这样就不会触发系统的自动包装:
- 在SwiftUI视图中用
fullScreenCover实现自定义全屏:
struct VideoPlayerView: View { @State private var isFullScreen = false @State private var player: AVPlayer? = AVPlayer(url: URL(string: "your-video-url")!) var body: some View { VStack { // 内嵌的播放器 PlayerContainer(player: $player) .frame(height: 300) .cornerRadius(8) Button(isFullScreen ? "退出全屏" : "进入全屏") { isFullScreen.toggle() } } .fullScreenCover(isPresented: $isFullScreen) { // 全屏状态下的播放器 PlayerContainer(player: $player) .edgesIgnoringSafeArea(.all) } } }
这种方式下,AVPlayerViewController始终在你自己控制的层级里,IMA广告的父控制器不会出现不匹配的情况,完全避开了系统的AVFullScreenViewController坑。
方案三:使用IMA官方的IMAAVPlayerVideoDisplay适配
Google IMA SDK专门提供了IMAAVPlayerVideoDisplay来和AVPlayer深度集成,它会自动跟踪播放器的视图层级变化,包括全屏场景,不用你手动处理父控制器:
- 用
IMAAVPlayerVideoDisplay替代直接用playerVC的view作为容器:
func makeUIViewController(context: Context) -> AVPlayerViewController { let playerVC = AVPlayerViewController() let player = AVPlayer(url: URL(string: "your-video-url")!) playerVC.player = player // 初始化IMA的视频显示组件 let videoDisplay = IMAAVPlayerVideoDisplay(avPlayer: player) let adDisplayContainer = IMAAdDisplayContainer(adContainer: playerVC.view, viewController: playerVC) // 加载广告 let adLoader = IMAAdLoader(settings: nil) let adRequest = IMAAdRequest(adTagUrl: "your-ad-tag-url") adLoader.requestAds(with: adRequest, adDisplayContainer: adDisplayContainer, videoDisplay: videoDisplay, delegate: context.coordinator) return playerVC }
这个方案是Google官方推荐的AVPlayer集成方式,IMA SDK内部已经处理了各种视图层级变化的情况,包括全屏过渡,能最大程度避免UIKit异常。
总结
优先推荐方案三(用IMA官方的视频显示组件),因为这是SDK原生支持的适配逻辑;如果需要保留系统默认的全屏按钮,那么方案一是最稳妥的;如果对全屏交互有自定义需求,方案二会给你最大的控制权。
内容来源于stack exchange

