如何通过SceneDelegate监听ViewController前后台切换并控制AVPlayer启停
实现方案
核心逻辑
基于SceneDelegate的实现完全可以避开全局UIApplication通知,核心思路如下:
- 首先自定义一个协议,规定前后台切换时需要执行的方法,所有需要感知Scene活跃状态的ViewController都可以遵守这个协议
- 新增工具方法获取当前Scene的顶层显示ViewController
- 在SceneDelegate的生命周期回调中,拿到顶层VC后判断是否遵守了自定义协议,执行对应逻辑即可
- 不需要在SceneDelegate里硬编码判断ViewController类型,后续新增需要监听的页面只要遵守协议即可,维护性更高
具体代码实现
1. 定义前后台感知协议
新建一个Swift文件或者直接在IntroViewController上方添加如下代码:
@objc protocol SceneActivityAware: AnyObject { @objc optional func sceneWillResignActive() @objc optional func sceneDidBecomeActive() // 如果需要只监听真正进入后台/返回前台,可新增下面两个方法 @objc optional func sceneDidEnterBackground() @objc optional func sceneWillEnterForeground() }
2. 新增获取顶层ViewController的工具扩展
同样可以放在公共文件或者SceneDelegate文件上方:
extension UIWindow { func topMostViewController() -> UIViewController? { var top = self.rootViewController while let presentedViewController = top?.presentedViewController { top = presentedViewController } return top } }
3. 修改SceneDelegate代码
完善对应的生命周期回调:
import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { guard let _ = (scene as? UIWindowScene) else { return } } func sceneDidDisconnect(_ scene: UIScene) { } func sceneDidBecomeActive(_ scene: UIScene) { // 获取当前顶层VC,执行协议方法 guard let topVC = window?.topMostViewController() as? SceneActivityAware else { return } topVC.sceneDidBecomeActive?() } func sceneWillResignActive(_ scene: UIScene) { // 获取当前顶层VC,执行协议方法 guard let topVC = window?.topMostViewController() as? SceneActivityAware else { return } topVC.sceneWillResignActive?() } func sceneWillEnterForeground(_ scene: UIScene) { // 仅切回前台才触发的逻辑走这里 guard let topVC = window?.topMostViewController() as? SceneActivityAware else { return } topVC.sceneWillEnterForeground?() } func sceneDidEnterBackground(_ scene: UIScene) { // 仅真正进入后台才触发的逻辑走这里 guard let topVC = window?.topMostViewController() as? SceneActivityAware else { return } topVC.sceneDidEnterBackground?() } }
说明:如果你的需求是只要App失去焦点(比如来电、上拉控制中心)就暂停视频,就用
sceneWillResignActive/sceneDidBecomeActive;如果只有真正切到后台再切回才需要处理,就用sceneDidEnterBackground/sceneWillEnterForeground,按需选择即可。
4. 修改IntroViewController代码
让它遵守SceneActivityAware协议,实现对应方法,同时删掉原来的全局通知监听相关代码:
import Foundation import UIKit import AVKit // 遵守自定义协议 class IntroViewController : UIViewController, SceneActivityAware { @IBOutlet weak var videoView: UIView! @IBOutlet weak var introLabel: UILabel! var player: AVPlayer? // 记录当前是否应该播放,避免页面未加载完成时误触发播放 private var isPlayerReady = false override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) playVideo() isPlayerReady = true } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) isPlayerReady = false if let player = player { player.pause() } player = nil } private func playVideo() { if player != nil { player!.play() } else { guard let path = Bundle.main.path(forResource: "intro_video", ofType:"mp4") else { debugPrint("intro.MP4 not found") return } player = AVPlayer(url: URL(fileURLWithPath: path)) player!.actionAtItemEnd = .none let playerFrame = CGRect(x: 0, y: 0, width: videoView.frame.width, height: videoView.frame.height) NotificationCenter.default.addObserver(self, selector: #selector(playerItemDidReachEnd(notification:)), name: .AVPlayerItemDidPlayToEndTime, object: player!.currentItem) let playerController = AVPlayerViewController() playerController.player = player playerController.videoGravity = .resizeAspectFill playerController.view.frame = playerFrame playerController.showsPlaybackControls = false addChild(playerController) videoView.addSubview(playerController.view) playerController.didMove(toParent: self) player!.play() player!.rate = 1.3 } } // MARK: - SceneActivityAware 协议实现 func sceneWillResignActive() { guard isPlayerReady, let player = player else { return } player.pause() } func sceneDidBecomeActive() { guard isPlayerReady, let player = player else { return } player.play() } @objc func playerItemDidReachEnd(notification: Notification) { let mainViewController = self.storyboard!.instantiateViewController(withIdentifier: "main_view_controller") as! MainViewController player!.pause() present(mainViewController, animated:true, completion:nil) } @IBAction func skipVideo(_ sender: Any) { if player != nil { player?.pause() player = nil } let mainViewController = self.storyboard!.instantiateViewController(withIdentifier: "main_view_controller") as! MainViewController present(mainViewController, animated:true, completion:nil) } }
问题解答
- 当ViewController因返回主菜单等场景进入后台时,
sceneWillResignActive会先触发,此时通过工具方法拿到的顶层VC如果是当前的IntroViewController,就会自动调用它的sceneWillResignActive方法完成事件检测。 - 不需要在SceneDelegate里硬编码判断ViewController类型,通过协议的方式可以自动匹配所有需要感知状态的VC,后续新增其他需要监听的页面只要遵守
SceneActivityAware协议实现对应方法即可,不需要修改SceneDelegate代码。
内容的提问来源于stack exchange,提问作者El_Loco
相关产品推荐
相关产品推荐

