6

I want to have a listener of sorts that reports whenever a new UIViewController is pushed. I can achieve this by subclassing from a single class and then listening in super viewDidLoad/viewDidAppear calls. But I would still have to pass the subclass name to super.

Is there any other way to automatically detect whenever any new view appears?

Context of it is that I am working on a logging library that reports screen load time etc. I also want to listen to any button tap ever in the app at a single point.

Lamour
  • 3,002
  • 2
  • 16
  • 28
frigocat
  • 283
  • 2
  • 13

4 Answers4

10

For logging purposes, you don't need to subclass or tediously add code to every UIViewController instance. Instead, swizzle the UIViewController's viewDidAppear method with your own.

private let swizzling: (AnyClass, Selector, Selector) -> () = { forClass, originalSelector, swizzledSelector in
    let originalMethod = class_getInstanceMethod(forClass, originalSelector)
    let swizzledMethod = class_getInstanceMethod(forClass, swizzledSelector)
    method_exchangeImplementations(originalMethod!, swizzledMethod!)
}

extension UIViewController {

    static let classInit: Void = {
        let originalSelector = #selector(viewDidAppear(_:))
        let swizzledSelector = #selector(swizzledViewDidAppear(_:))
        swizzling(UIViewController.self, originalSelector, swizzledSelector)
    }()

    @objc func swizzledViewDidAppear(_ animated: Bool) {
        print("Add your logging logic here")
        // Call the original viewDidAppear - using the swizzledViewDidAppear signature
        swizzledViewDidAppear(animated)
    }

}

Note you'll have to kick-off the swizzle by overriding the AppDelegate's init for Swift 4.

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    override init() {
        super.init()
        UIViewController.classInit
    }

Original credits to @efremidze and @TikhonovAlexander

Jalakoo
  • 3,523
  • 2
  • 21
  • 20
  • 2
    swizzling is really dangerous and should be really carefully used... Only for pro's who fully understand the meaning and not just for copy paste answer for novice developers. – Kashkashio Jun 14 '20 at 10:07
  • I'm using swizzling to trace the app flow from one vc to another, as I continue working on app done previously, in the swizzledViewDidLoad I do this `@objc func swizzledViewDidLoad() {print("current vc loaded \(self.debugDescription)") swizzledViewDidLoad()}` and I can see the VC class name as I go through the app – Dan Jun 18 '23 at 06:07
0

How about using the Observable pattern here? Setup each view controller to notify the appdelegate about changes in its life cycle methods. You can then log these changes into a file if needed.

Rakshith Nandish
  • 599
  • 3
  • 13
0

I think that, as you mentioned, the best approach would be to subclass a view controller and do the logging part in the viewDidLoad/viewWillDisappear methods. However, you do not need to pass the subclass name to super on each subclass to achieve what you want. What you can do instead is the following:

In the ViewController which you will subclass, override the viewDidLoad function:

override func viewDidLoad() {
    super.viewDidLoad()

    NotificationCenter.default.post(name: Notification.Name(rawValue: "ViewDidLoad"), object: nil, userInfo: [
        "name": NSStringFromClass(type(of: self))
    ])
}

This will post a notification with the corresponding view controllers name in the userInfo object. Then, you can subscribe to this notification at a central place (e.g., the AppDelegate) and handle the events.

Lukas
  • 607
  • 4
  • 9
0

A not-so-pretty implementation would be to subclass all of your view controllers so that they automatically send the events to an observer using notifications or simply running a method on a singleton.

class ViewLifeCycleObserver {
    static let shared = ViewLifeCycleObserver()
    private(set) var viewWillAppearControllerNames: [String] = []
    private(set) var viewDidLoadControllerNames: [String] = []
    private init(){

    }
    func viewDidLoad(inViewController viewController: UIViewController){
        viewDidLoadControllerNames.append(viewController.className)
        print(viewController.className)
    }
    func viewWillAppear(inViewController viewController: UIViewController){
        viewWillAppearControllerNames.append(viewController.className)
        print(viewController.className)
    }
}

class ViewWillAppearObservable: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        ViewLifeCycleObserver.shared.viewDidLoad(inViewController: self)
    }
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        ViewLifeCycleObserver.shared.viewWillAppear(inViewController: self)
    }
}
extension UIViewController {
    var className: String {
        return NSStringFromClass(self.classForCoder).components(separatedBy: ".").last!
    }
}

If subclassing is not an option, you can add the code in the ViewWillAppearObservable class to each and every UIViewController

DatForis
  • 1,331
  • 12
  • 19