Search code examples
iosobjective-cscreendetection

How to detect which view is appeared without modifying the code in ViewControllers but only AppDelegate


Is there a way to accomplish this? Like let's say a framework perhaps?

All I need is something like:

aViewController appeared
bTableViewController appeared
cViewController appeared

Like I added a code in each viewDidLoad, but without adding that code. I know that there is a visibleViewController if I used navigation embed but I don't and I can't.

The framework I'm working on is based on no baseline, so I can not assume anything, like I'll past a code in a project and when the user compiles the code, I will get the views.

The purpose is I'm creating an analytic tool without being have to add individual code in each view controllers's viewDidLoad method.


Solution

  • First of all, there's no such thing as a "visible view controller" technically speaking, despite the annoyingly named method of navigation controllers. Instead, we should be talking about visible views. There's nothing visible about the controller.

    But what you really want to know is what view controller is active and controlling visible views. With a navigation controller, it's the top of the nav-stack, with a tab bar controller, it's the selected view controller, etc.

    By implementing ONLY code in app-delegate, I don't know of any way to accomplish what you're trying to accomplish.

    Instead, what would be your best option would be to provide a framework that included a subclass of UIViewController and UIAppDelegate.

    Set the code in these to do all the analytical work necessary. Provide this framework as a whole and inform your users that if they want to make use of the analytics you've provide, they'll need to subclass your view controller instead of UIViewController, etc.

    In your custom classes, just override viewDidAppear: to send a notification and viewDidDisappear: to send a notification. And the appdelegate just manages an array that keeps track of which is on top, adding when the viewDidAppear: notification fires, and removing when the viewDidDisappear: notification fires.

    And to be sure your viewDidAppear:/viewDidDisappear: isn't overridden, be sure to make use of this: When a subclass overrides a method, how can we ensure at compile time that the superclass's method implementation is called?


    So in your view controller base class, it'd be as simple as posting a notification:

    // AnalyticViewController.h
    
    @interface AnalyticViewController : UIViewController
    - (void)viewDidAppear NS_REQUIRES_SUPER;
    - (void)viewDidDisappear NS_REQUIRES_SUPER;
    @end
    
    // AnalyticViewController.m
    
    @implementation AnalyticViewController
    
    - (void)viewDidAppear:(BOOL)animated {
        [super viewDidAppear:animated];
        [[NSNotificationCenter defaultCenter] postNotificationName:@"AnalyticVCAppeared"
                                                            object:self
                                                          userInfo:nil];
    }
    
    - (void)viewDidDisappear:(BOOL)animated {
        [super viewDidAppear:animated];
        [[NSNotificationCenter defaultCenter] postNotificationName:@"AnalyticVCDisappeared"
                                                            object:self
                                                          userInfo:nil];
    }
    
    @end
    

    And register for these in the App delegate in application:didFinishLaunchingWithOptions:. You'll want an mutable array to keep track of the stack of view controllers. And you'll want separate methods for each notification.

    Given a property @property (nonatomic,strong) NSMutableArray *analyzedViewControllers;, the methods that respond to the notifications should look like this:

    - (void)handleViewAppear:(NSNotification *)aNotification {
        if (!self.analyzedViewControllers) {
            self.analyzedViewControllers = [NSMutableArray array];
        }
        [self.analyzedViewControllers addObject:[aNotification object]];
    }
    
    - (void)handleViewDisappeared:(NSNotification *)aNotification {
        [self.analyzedViewControllers removeObject:[aNotification object]];
    }
    

    And the top-most view controller will always be at [self.analyzedViewControllers lastObject].


    If you need to send more information about the view controller, you can using the userInfo argument when posting the notification. You could even put the entire notification into the array, so you have the view controller reference and the user info reference.

    What can also be important to note however is that just because one view controller has appeared doesn't inherently mean another has disappeared. View controllers don't necessarily take up the entire screen, and although there's eventually a practical limit to how many view controllers have visible views on screen, it's a quite high limit.


    ADDENDUM: To add some clarity... in the AnalyticViewController class I'm recommending, we have this line:

    @interface AnalyticViewController : UIViewController
    

    A line similar to this exists in every one of your view controller's header files. What I'm recommending is that all your other view controllers subclass this one, so instead of:

    @interface FooBarViewController : UIViewController
    

    You'd instead have this:

    #import AnalyticViewController.h
    @interface FooBarViewController : AnalyticViewController
    

    And the code in the viewDidAppear: and viewDidDisappear: of AnalyticViewController is now automatically in FooBarViewController unless you ignore the NS_REQUIRES_SUPER warning and override the viewDidAppear: or viewDidDisappear: methods.

    It will be a little bit of effort to change over existing view controllers, but for any future view controllers that are not yet created, it's as simple as choosing to subclass AnalyticViewController rather than UIViewController when you create the file.