Detect if the app was launched/opened from a push notification

The issue we had was in correctly updating the view after the app is launched. There are complicated sequences of lifecycle methods here that get confusing.

Lifecycle Methods

Our testing for iOS 10 revealed the following sequences of lifecycle methods for the various cases:

DELEGATE METHODS CALLED WHEN OPENING APP

    Opening app when system killed or user killed
        didFinishLaunchingWithOptions
        applicationDidBecomeActive

    Opening app when backgrounded
        applicationWillEnterForeground
        applicationDidBecomeActive

DELEGATE METHODS CALLED WHEN OPENING PUSH

    Opening push when system killed
        [receiving push causes didFinishLaunchingWithOptions (with options) and didReceiveRemoteNotification:background]
        applicationWillEnterForeground
        didReceiveRemoteNotification:inactive
        applicationDidBecomeActive

    Opening push when user killed
        didFinishLaunchingWithOptions (with options)
        didReceiveRemoteNotification:inactive [only completionHandler version]
        applicationDidBecomeActive

    Opening push when backgrounded
        [receiving push causes didReceiveRemoteNotification:background]
        applicationWillEnterForeground
        didReceiveRemoteNotification:inactive
        applicationDidBecomeActive

The problem

Ok, so now we need to:

  1. Determine if the user is opening the app from a push
  2. Update the view based on the push state
  3. Clear the state so that subsequent opens don't return the user to the same position.

The tricky bit is that updating the view has to happen when the application actually becomes active, which is the same lifecycle method in all cases.

Sketch of our solution

Here are the main components of our solution:

  1. Store a notificationUserInfo instance variable on the AppDelegate.
  2. Set notificationUserInfo = nil in both applicationWillEnterForeground and didFinishLaunchingWithOptions.
  3. Set notificationUserInfo = userInfo in didReceiveRemoteNotification:inactive
  4. From applicationDidBecomeActive always call a custom method openViewFromNotification and pass self.notificationUserInfo. If self.notificationUserInfo is nil then return early, otherwise open the view based on the notification state found in self.notificationUserInfo.

Explanation

When opening from a push didFinishLaunchingWithOptions or applicationWillEnterForeground is always called immediately before didReceiveRemoteNotification:inactive, so we first reset notificationUserInfo in these methods so there's no stale state. Then, if didReceiveRemoteNotification:inactive is called we know we're opening from a push so we set self.notificationUserInfo which is then picked up by applicationDidBecomeActive to forward the user to the right view.

There is one final case which is if the user has the app open within the app switcher (i.e. by double tapping the home button while the app is in the foreground) and then receives a push notification. In this case only didReceiveRemoteNotification:inactive is called, and neither WillEnterForeground nor didFinishLaunching gets called so you need some special state to handle that case.

Hope this helps.


See This code :

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
    if ( application.applicationState == UIApplicationStateInactive || application.applicationState == UIApplicationStateBackground  )
    {
         //opened from a push notification when the app was on background
    }
}

same as

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

late but maybe useful

When app is not running

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

is called ..

where u need to check for push notification

NSDictionary *notification = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
if (notification) {
    NSLog(@"app recieved notification from remote%@",notification);
    [self application:application didReceiveRemoteNotification:notification];
} else {
    NSLog(@"app did not recieve notification");
}

This is a well worn post... but it is still missing an actual solution to the problem (as is pointed out in the various comments).

The original question is about detecting when the app was launched / opened from a push notification, e.g. a user taps on the notification. None of the answers actually cover this case.

The reason can be seen in the call flow when a notification arrives, application:didReceiveRemoteNotification...

gets called when the notification is received AND again when the notification is tapped by the user. Because of this, you can't tell by just looking at UIApplicationState wether the user tapped it.

Additionally, you no longer need to handle the situation of a 'cold start' of the app in application:didFinishLaunchingWithOptions... as application:didReceiveRemoteNotification... is called again after launching in iOS 9+ (maybe 8 as well).

So, how can you tell if the user tap started the chain of events? My solution is to mark the time at which the app begins to come out of the background or cold start and then check that time in application:didReceiveRemoteNotification.... If it is less than 0.1s, then you can be pretty sure the tap triggered the startup.

Swift 2.x

class AppDelegate: UIResponder, UIApplicationDelegate {

  var wakeTime : NSDate = NSDate()        // when did our application wake up most recently?

  func applicationWillEnterForeground(application: UIApplication) {    
    // time stamp the entering of foreground so we can tell how we got here
    wakeTime = NSDate()
  }

  func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {
    // ensure the userInfo dictionary has the data you expect
    if let type = userInfo["type"] as? String where type == "status" {
      // IF the wakeTime is less than 1/10 of a second, then we got here by tapping a notification
      if application.applicationState != UIApplicationState.Background && NSDate().timeIntervalSinceDate(wakeTime) < 0.1 {
        // User Tap on notification Started the App
      }
      else {
        // DO stuff here if you ONLY want it to happen when the push arrives
      }
      completionHandler(.NewData)
    }
    else {
      completionHandler(.NoData)
    }
  }
}

Swift 3

class AppDelegate: UIResponder, UIApplicationDelegate {

    var wakeTime : Date = Date()        // when did our application wake up most recently?

    func applicationWillEnterForeground(_ application: UIApplication) {
      // time stamp the entering of foreground so we can tell how we got here
      wakeTime = Date()
    }

  func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {

      // ensure the userInfo dictionary has the data you expect
      if let type = userInfo["type"] as? String, type == "status" {
        // IF the wakeTime is less than 1/10 of a second, then we got here by tapping a notification
        if application.applicationState != UIApplicationState.background && Date().timeIntervalSince(wakeTime) < 0.1 {
          // User Tap on notification Started the App
        }
        else {
          // DO stuff here if you ONLY want it to happen when the push arrives
        }
        completionHandler(.newData)
      }
      else {
        completionHandler(.noData)
      }
    }
}

I have tested this for both cases (app in background, app not running) on iOS 9+ and it works like a charm. 0.1s is pretty conservative too, the actual value is ~0.002s so 0.01 is fine as well.