Test if app did become active from a UILocalNotification

26,007

Solution 1

I'm afraid Sylter is incorrect. When an app enters the foreground from the background, either by a direct user action or by a user response to a UILocalNotification, it does not trigger applicationDidFinishLaunchingWithOptions. It does, however, call applicationWillEnterForeground and applicationDidBecomeActive. This can be verified with a couple of NSLogs.

So, the problem remains: if an app is entering the foreground from the background, there is no way to discover if the app is entering the foreground in response to a user's response to a UILocalNotification, or if it is merely entering the foreground. Except...

Once the app has entered the foreground, it will receive the method application:DidReceiveLocalNotification: if the app entered the foreground in response to a UILocalNotification.

The problem is that any UI changes made within the application:DidReceiveLocalNotification: method in response to receiving the UILocalNotification occur after the app has already entered the foreground, creating a disturbing experience for the user.

Has anyone found a solution?

Solution 2

I got the clue to the solution for this from @naveed's tip on checking the state of the application when the didReceiveNotification method is called. No need to check variables etc when the app resumes from the background.

On iOS7 and lower you handle the notifications like this:

- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {
    if (application.applicationState == UIApplicationStateInactive ) {
         //The application received the notification from an inactive state, i.e. the user tapped the "View" button for the alert.
         //If the visible view controller in your view controller stack isn't the one you need then show the right one.
    }

    if(application.applicationState == UIApplicationStateActive ) { 
        //The application received a notification in the active state, so you can display an alert view or do something appropriate.
    }
}

Update for iOS 8: The following methods are now invoked when the app is opened from the background via a notification.

- (void)application:(UIApplication *)application handleActionWithIdentifier:(NSString *)identifier forLocalNotification:(UILocalNotification *)notification completionHandler:(void(^)())completionHandler {
}

- (void)application:(UIApplication *)application handleActionWithIdentifier:(NSString *)identifier forRemoteNotification:(NSDictionary *)userInfo completionHandler:(void(^)())completionHandler {
}

If the notifications are received while the app is in the foreground, use the methods:

- (void) application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *) userInfo {
}

- (void) application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification {
}

Note that there is no need to check application state unless you want to support older versions of the OS in your app.

Solution 3

You can check the scenarios of either application is running or not when application received by following.

- (void)application:(UIApplication *)app didReceiveLocalNotification:(UILocalNotification *)notif {
    if (app.applicationState == UIApplicationStateInactive ) {
        NSLog(@"app not running");
    }else if(app.applicationState == UIApplicationStateActive )  {
        NSLog(@"app running");      
    }

    // Handle the notificaton when the app is running
    NSLog(@"Recieved Notification %@",notif);
}

Solution 4

What I did was, I tested two scenarios, one is to put the app back to the foreground by clicking on the icon, another by URL sys call, and compared all the variables inside UIApplication, and surprisingly I finally found what I was looking for in UIApplication.h:

struct {
    unsigned int isActive:1;
    unsigned int isSuspended:1;
    unsigned int isSuspendedEventsOnly:1;
    unsigned int isLaunchedSuspended:1;
    unsigned int calledNonSuspendedLaunchDelegate:1;
    unsigned int isHandlingURL:1;
    unsigned int isHandlingRemoteNotification:1;
    unsigned int isHandlingLocalNotification:1;
    unsigned int statusBarShowsProgress:1;
    unsigned int statusBarRequestedStyle:4;
    unsigned int statusBarHidden:1;
    unsigned int blockInteractionEvents:4;
    unsigned int receivesMemoryWarnings:1;
    unsigned int showingProgress:1;
    unsigned int receivesPowerMessages:1;
    unsigned int launchEventReceived:1;
    unsigned int isAnimatingSuspensionOrResumption:1;
    unsigned int isResuming:1;
    unsigned int isSuspendedUnderLock:1;
    unsigned int isRunningInTaskSwitcher:1;
    unsigned int shouldExitAfterSendSuspend:1;
    unsigned int shouldExitAfterTaskCompletion:1;
    unsigned int terminating:1;
    unsigned int isHandlingShortCutURL:1;
    unsigned int idleTimerDisabled:1;
    unsigned int deviceOrientation:3;
    unsigned int delegateShouldBeReleasedUponSet:1;
    unsigned int delegateHandleOpenURL:1;
    unsigned int delegateOpenURL:1;
    unsigned int delegateDidReceiveMemoryWarning:1;
    unsigned int delegateWillTerminate:1;
    unsigned int delegateSignificantTimeChange:1;
    unsigned int delegateWillChangeInterfaceOrientation:1;
    unsigned int delegateDidChangeInterfaceOrientation:1;
    unsigned int delegateWillChangeStatusBarFrame:1;
    unsigned int delegateDidChangeStatusBarFrame:1;
    unsigned int delegateDeviceAccelerated:1;
    unsigned int delegateDeviceChangedOrientation:1;
    unsigned int delegateDidBecomeActive:1;
    unsigned int delegateWillResignActive:1;
    unsigned int delegateDidEnterBackground:1;
    unsigned int delegateWillEnterForeground:1;
    unsigned int delegateWillSuspend:1;
    unsigned int delegateDidResume:1;
    unsigned int userDefaultsSyncDisabled:1;
    unsigned int headsetButtonClickCount:4;
    unsigned int isHeadsetButtonDown:1;
    unsigned int isFastForwardActive:1;
    unsigned int isRewindActive:1;
    unsigned int disableViewGroupOpacity:1;
    unsigned int disableViewEdgeAntialiasing:1;
    unsigned int shakeToEdit:1;
    unsigned int isClassic:1;
    unsigned int zoomInClassicMode:1;
    unsigned int ignoreHeadsetClicks:1;
    unsigned int touchRotationDisabled:1;
    unsigned int taskSuspendingUnsupported:1;
    unsigned int isUnitTests:1;
    unsigned int requiresHighResolution:1;
    unsigned int disableViewContentScaling:1;
    unsigned int singleUseLaunchOrientation:3;
    unsigned int defaultInterfaceOrientation:3;
} _applicationFlags;

This contains possibly all the information a programmer wish they have access to when the application returns to the foreground, to be in particular, I would like to access the flag "isHandlingURL" which says 1 if the app is put into foreground by a sys-call, 0 if the app is put into foreground by the user.

Next, I looked at the address of "application" and "_applicationFlags", noticed that they are offset by 0x3C, which is 60, so I decided to use address operations to get my needed bit:

- (void)applicationWillEnterForeground:(UIApplication *)application
{
    /*
     Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
     */
    id* app = [UIApplication sharedApplication];
    app = app+15; //address increments by long words, don't know if it will be the same on device
    NSLog(@"Test:%x",*app);
}

which prints out test:4a40012, or 0x04a40012 if I write in a complete long word format. This gives me in binary 0000 0100 1010 0100 0000 0000 0001 0010. Looking back into _applicationFlags, this will give us "isHandlingURL" on 6th bit from LSB, which is 0. Now if I try to put the app into background and bring it back with a URL sys call, I get a print out of 4a40032 which in binary is 0000 0100 1010 0100 0000 0000 0011 0010 and I have my isHandlingURL bit turned on! So all that there is left to do is to complete the statement by bit-shift operations, and the final code will look like:

- (void)applicationWillEnterForeground:(UIApplication *)application
{
    /*
     Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background.
     */
    id* app = (id*)[UIApplication sharedApplication]+15;
    BOOL isHandlingURL = ((Byte)*app>>5&0x1);
    if (isHandlingURL) {
        //do whatever I wanna do here
    }
}

I can go on and write a complete function to parse out all the _applicationFlag, but then it is at this point uncertain if the address increment is fixed to be 15 on both the simulator and the target, my next goal will be to replace with magic number '15' by some macro defines or values from the system so I can be sure that it will always shift 0x3C as required, and I need to look into UIApplication header to make sure that the _applicationFlag will always shift by 0x3C.

That's all for now!

Solution 5

In your AppDelegate:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    

    // Override point for customization after application launch.

    UILocalNotification *localNotif = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey];

    if (localNotif) {
        NSLog(@"Recieved Notification %@",localNotif);
    //Do Something
    } else {
    //Do Something else if I didn't recieve any notification, i.e. The app has become active
    }

    return YES;
}

Or, if you want to know when the application is in foreground or in background you can use this method:

- (void)applicationWillResignActive:(UIApplication *)application {
    /*
     Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
     Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
     */
} 

- (void)applicationDidEnterBackground:(UIApplication *)application {
    /*
     Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 
     If your application supports background execution, called instead of applicationWillTerminate: when the user quits.
     */
}


- (void)applicationWillEnterForeground:(UIApplication *)application {
    /*
     Called as part of  transition from the background to the active state: here you can undo many of the changes made on entering the background.
     */
}


- (void)applicationDidBecomeActive:(UIApplication *)application {
    /*
     Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
     */
}


- (void)applicationWillTerminate:(UIApplication *)application {
    /*
     Called when the application is about to terminate.
     See also applicationDidEnterBackground:.
     */
}
Share:
26,007

Related videos on Youtube

wh1t3cat1k
Author by

wh1t3cat1k

Updated on November 29, 2020

Comments

  • wh1t3cat1k
    wh1t3cat1k over 3 years

    Is there a way to know if the application did become active from a local notification?

    I know there is a way to test if the application was launched from a local notification alert; but if it just was sitting out there the background, and received a notification?

    I need to run different code, when the app has become active:

    1. From a local notification.
    2. Just has become active :)

    Is there a way to do it?

  • wh1t3cat1k
    wh1t3cat1k over 13 years
    Is this method (applicationDidFinishLaunchingWithOptions) called when the app restores from the background in the iPhone 4?
  • Rengers
    Rengers over 13 years
    No, that would be applicationWillEnterForeground, followed by applicationDidBecomeActive.
  • Sylter
    Sylter over 13 years
    Right! You have to use applicationWillEnterForeground and applicationDidBecomeActive: in these methods you can undo many of the changes made on entering the background or you can refresh the UI
  • wh1t3cat1k
    wh1t3cat1k over 13 years
    Yes, but how do I know if the app has entered foreground from a local notification or not?
  • Sylter
    Sylter over 13 years
    If the app has entered foreground from a local notification, it calls - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions method, so with the code that I've posted you can know everything: - applicationDidEnterBackground: app entered in background - applicationWillEnterForeground/applicationDidBecomeActive: app become active, so users can use it. - - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions: when the app is launched, so even if it was launched by a Local Notification.
  • Matt Connolly
    Matt Connolly over 12 years
    Bingo!!!!!!!!!! (needed the exclamation points to reach the minimum comment length...)
  • Philip Kramer
    Philip Kramer over 12 years
    i thing apple dont allow you to access _applicationFlags, and no metter if you want access it by its pointer.
  • aporat
    aporat about 12 years
    UILocalNotification *localNotif = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey]‌​; if (localNotif) { NSLog(@"local notification detected!"); [self application:application didReceiveLocalNotification:localNotif]; }
  • SAHM
    SAHM over 11 years
    Did you submit this to the app store? Was it approved?
  • Markus Rautopuro
    Markus Rautopuro almost 10 years
    I'm currently testing this on iOS 8 beta 5 and it seems that the behavior has been changed. When the application is running in background and you get a local notification, and open the app from it, didReceiveLocalNotification: is not called after applicationDidBecomeActive:.
  • UberJason
    UberJason almost 10 years
    I also observe what Markus is seeing... if the app is in the background and you get a local notification and open the app from it, didReceiveLocalNotification: is never called. I'm also on iOS 8 beta 5.
  • UberJason
    UberJason almost 10 years
    Oh! BUT, I was implementing the new iOS 8 actionable notifications, and I noticed that application:handleActionWithIdentifier:forLocalNotification:‌​completionHandler: is called. (Not just if an action is tapped, but if the notification itself is tapped.) Hope this helps anyone else reading this in the future.
  • Michael Gaylord
    Michael Gaylord almost 10 years
    I'll update my answer to reflect this. Thanks for letting me know.
  • UberJason
    UberJason almost 10 years
    Just to follow up on this: I've discovered that on iOS 8 GM, the behavior returns to the original behavior. That is, didReceiveLocalNotification: is called in response to a notification.
  • Andreas
    Andreas almost 9 years
    Yeah, this doesn't work in iOS 8.4, handleActionWithIdentifier: forLocalNotification: is not called when tapping a local notification.
  • Phil Calvin
    Phil Calvin almost 8 years
    This is the kind of C knowledge you want to save for debugging and avoid when writing code. Please, please, please don't code against exact memory offsets in a library you don't control (UIKit) that doesn't guarantee ABI stability for these private members.
  • i.AsifNoor
    i.AsifNoor about 7 years
    I tested it on ios8 and by using UIMutableUserNotificationActiion. If I clicked on notification text instead of action button in local notification, In didFinishLaunchingWithOptions method, I was able to extract notification info UILocalNotification *localNotification = [launchOptions objectForKey:UIApplicationLaunchOptionsLocalNotificationKey]‌​; which means I could tell that it was launched with local notificaiton. But if I clicked on action button then it was not possible to tell. P.S UIMutableUserNotificationAction will not work in iOS 10.