Want to create a listener that detects viewWillAppear calls throughout the app
Asked Answered
H

4

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.

Horripilation answered 26/9, 2017 at 2:46 Comment(0)
O
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

Organelle answered 26/9, 2017 at 7:5 Comment(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.Mccoy
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 appQuintana
O
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.

Ornithine answered 26/9, 2017 at 5:55 Comment(0)
S
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.

Sandwich answered 26/9, 2017 at 6:10 Comment(0)
R
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

Rajasthan answered 26/9, 2017 at 6:33 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.