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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 15:21:26