iOS开发中如何监听页面跳转时当前可见ViewController的变化?
无侵入监听ViewController切换实现方案
KVO监听rootViewController无法收到回调属于正常现象:UIKit内部对该属性的部分赋值流程不会触发KVO通知,且作为组件库无法修改主工程代码的场景下,Runtime方法交换(Method Swizzling)是唯一稳定、无侵入的实现方案,也是Firebase Analytics官方实际采用的实现逻辑。
核心实现思路
不需要单独监听rootViewController的变更,只要覆盖所有ViewController展示的入口即可,所有场景(push/pop导航切换、present/dismiss模态弹出、TabBar切换选中页、window更换rootViewController)下,当前可见的ViewController最终一定会调用viewDidAppear(_:)方法,交换该方法即可100%覆盖所有页面切换场景,无漏报。
具体实现代码
1. 交换UIViewController的viewDidAppear方法
在组件库初始化阶段自动执行一次方法交换,不需要主工程编写任何适配代码:
import UIKit // 方法交换全局只执行一次 private let vcLifecycleSwizzle: Void = { let originalSelector = #selector(UIViewController.viewDidAppear(_:)) let swizzledSelector = #selector(UIViewController.track_viewDidAppear(_:)) guard let originalMethod = class_getInstanceMethod(UIViewController.self, originalSelector), let swizzledMethod = class_getInstanceMethod(UIViewController.self, swizzledSelector) else { return } method_exchangeImplementations(originalMethod, swizzledMethod) }() extension UIViewController { @objc func track_viewDidAppear(_ animated: Bool) { // 先执行原方法逻辑,不侵入原有业务流程 self.track_viewDidAppear(animated) // 过滤容器类VC、系统私有VC,只统计实际展示的业务VC let containerVCList = [UINavigationController.self, UITabBarController.self, UISplitViewController.self] for containerType in containerVCList { if self.isKind(of: containerType) { return } } if NSStringFromClass(type(of: self)).hasPrefix("_") { return } // 此处可获取当前展示的VC类名,触发屏幕上报逻辑 let screenClassName = NSStringFromClass(type(of: self)) // 自定义上报逻辑写在这里 } }
2. (可选)单独监听rootViewController变更
如果需要单独感知window的rootViewController更换事件,不需要用KVO,直接交换UIWindow的rootViewController属性setter即可,稳定性远高于KVO:
private let windowRootSwizzle: Void = { let originalSelector = #selector(setter: UIWindow.rootViewController) let swizzledSelector = #selector(UIWindow.track_setRootVC(_:)) guard let originalMethod = class_getInstanceMethod(UIWindow.self, originalSelector), let swizzledMethod = class_getInstanceMethod(UIWindow.self, swizzledSelector) else { return } method_exchangeImplementations(originalMethod, swizzledMethod) }() extension UIWindow { @objc func track_setRootVC(_ newViewController: UIViewController?) { // 先执行原setter逻辑 self.track_setRootVC(newViewController) // rootViewController变更后的回调逻辑写在这里 } }
注意事项
- 所有方法交换逻辑只需要执行1次,可以在Swift的
+initialize桥接方法、或OC的+load方法中自动触发,完全不需要主工程主动调用。 - 获取顶层可见VC时不要只取
presentedViewController,需要递归判断:如果当前是导航控制器取topViewController,如果是TabBar控制器取selectedViewController,如果存在presentedViewController就递归遍历,直到拿到最顶层的非容器VC。 - 记得过滤系统私有类、系统弹窗类VC,避免上报非业务页面。
内容的提问来源于stack exchange,提问作者Vivek Vashistha
相关产品推荐
相关产品推荐

