42
application: didReceiveRemoteNotification: fetchCompletionHandler:

is different from

application: didReceiveRemoteNotification:

How? from the docs:

Unlike the application:didReceiveRemoteNotification: method, which is called only when your app is running, the system calls this method regardless of the state of your app. If your app is suspended or not running, the system wakes up or launches your app and puts it into the background running state before calling the method. If the user opens your app from the system-displayed alert, the system calls this method again so that you know which notification the user selected.

My struggle is: I want to know if the method was called by the user tapping an a system-displayed alert from the Notification Center or from a silent push notification that wakes up the device. Currently, as far as I can see, there is no obvious way to differentiate.

- (BOOL)application: didFinishLaunchingWithOptions:

Tracking the launchOptions in the above method is not a solution because it's only called if the app is suspended/not running in background. If it's running in the background it doesn't get called.

Devfly
  • 2,495
  • 5
  • 38
  • 56

8 Answers8

45

The Apple docs are a bit confusing

application: didReceiveRemoteNotification: fetchCompletionHandler:  

is used if your application supports the remote-notification background mode (ie you're doing BackgroundFetch.)

application: didReceiveRemoteNotification:  

is called when the OS receives a RemoteNotification and the app is running (in the background/suspended or in the foreground.)
You can check the UIApplicationState to see if the app was brought to foreground by the user (tapping on notification) or was already running when notification comes in.

- (void) application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo {
      UIApplicationState state = [application applicationState];
        // user tapped notification while app was in background
    if (state == UIApplicationStateInactive || state == UIApplicationStateBackground) {
         // go to screen relevant to Notification content
    } else {
         // App is in UIApplicationStateActive (running in foreground)
         // perhaps show an UIAlertView
    }
}
hasan
  • 23,815
  • 10
  • 63
  • 101
race_carr
  • 1,387
  • 12
  • 21
  • 2
    I don't care about `application: didReceiveRemoteNotification:` but about `application: didReceiveRemoteNotification: fetchCompletionHandler:` – Devfly Apr 20 '14 at 01:47
  • So you're doing background fetching? I guess I am confused as to where lies the confusion. The Apple docs state (if your app support `remote-notification` background mode) >the system calls this method regardless of the state of your app. The docs also state >If the user opens your app from the system-displayed alert, the system calls this method again so that you know which notification the user selected. I would assume that tracking the UIApplicationState would still be applicable... – race_carr Apr 20 '14 at 16:13
  • 1
    @hasan83 - what will happen if I include NSString * jsCallBack = [NSString stringWithFormat:@"testfromdelegate()"]; [self.viewController.webView stringByEvaluatingJavaScriptFromString:jsCallBack]; inside if (state == UIApplicationStateInactive || state == UIApplicationStateBackground) { // go to screen relevant to Notification content } In both cases, I am expecting the JSCallback will be executed. Is that true? Please let me know. – Lohith Korupolu Jul 09 '15 at 11:55
  • which/what both cases? – hasan Jul 09 '15 at 12:06
  • @hasan83 - both cases UIApplicationStateInactive and UIApplicationStateBackground? – Lohith Korupolu Jul 09 '15 at 12:30
  • 1
    that's theoretically true. but, if notification launched the app from the InActive state (closed). that means that you don't have ur ref. to self.viewController initialised in your app delegate yet. got it?or shall I explain more? – hasan Jul 09 '15 at 12:35
  • hmm, I am not really sure. it depend on how you initialise your view controller and where. – hasan Jul 09 '15 at 12:38
  • Ah! I see the point!! Even if I am invoking the app from background, my JS callback is not working :-( I am trying to achieve this in my cordova app. I have my question posted here - http://stackoverflow.com/questions/30701615/ios-push-notification-click-handler-when-the-app-is-not-running, if you have some time? – Lohith Korupolu Jul 09 '15 at 12:42
  • I am on the chat, as suggested by SO - could you look at my SO question when u have some time? [continue this discussion in chat](http://chat.stackoverflow.com/rooms/82819/discussion-between-lohith-krishna-and-hasan83). – Lohith Korupolu Jul 09 '15 at 12:43
  • Swift version of this approach: `let state = application.applicationState let wasInBackground = (state == .Inactive || state == .Background)` – FouZ Feb 29 '16 at 15:41
  • application: didReceiveRemoteNotification: is only called when the app is on foreground https://developer.apple.com/reference/uikit/uiapplicationdelegate/1623013-application?language=objc – nicoabie Sep 23 '16 at 18:25
  • 1
    I don't know if it's an iOS 10 thing or what, but `-application: didReceiveRemoteNotification:` was **never** being fired, even when the app was running in the foreground (I did get banners in the home screen, though). I switched to `application: didReceiveRemoteNotification: fetchCompletionHandler:` and it works... I'm using CloudKit subscription-based notifications. – Nicolas Miari Oct 06 '16 at 06:35
  • for iOS 9: not iOS 10: How do you differentiate between a 'tapping on a notification' and 'just receiving'? I'm asking this for this scenario: I have a notification that upon arriving I need to 1. download something 2. **only if user has tapped** show a different viewController. @hasan83 – mfaani Jun 27 '17 at 15:42
  • You should be able to figure that out in the UIApplicationDelegate method `application(_:willFinishLaunchingWithOptions:) ` or by observing the `UIApplicationDidFinishLaunching` notification and accessing the attached `userInfo` dictionary – race_carr Jun 29 '17 at 14:27
18

You could check the UIApplication's applicationState to distinguish silent calls from calls made with the application being actively used by the user:

typedef enum : NSInteger {
   UIApplicationStateActive,
   UIApplicationStateInactive,
   UIApplicationStateBackground
} UIApplicationState;

Or keep your own flag set on the delegate's applicationDidEnterBackground:.

Rivera
  • 10,792
  • 3
  • 58
  • 102
  • 11
    The important bit: `Active`=App was open when notification was received, `Inactive`=User clicked on notification, `Background`=App was running in background when notification was received (content-available must be true; this case is the actual background fetching). Note that the app has to be running at least in the background. If you force-close the app, `didReceiveRemoteNotification` is not called. – nyi Nov 07 '14 at 15:21
  • 2
    You can no longer do this in iOS 10+ when you enable notifications to be shown while your app runs. http://stackoverflow.com/questions/23168345/detect-if-application-didreceiveremotenotification-fetchcompletionhandler-was#comment70360053_25548078 – Rivera Jan 10 '17 at 20:21
9

Application State is not reliable because if you have control center or Apple's notification center open over your app, application: didReceiveRemoteNotification: fetchCompletionHandler: will get called and the application state will be Inactive.

I'm having the same issue trying to respond to a click on the notification while the app is in the background and there doesn't seem to be a reliable way to solely identify this.

overunder
  • 131
  • 1
  • 4
4

In case of swift

func application(application: UIApplication, didReceiveRemoteNotification userInfo: [NSObject : AnyObject]) {

    let state : UIApplicationState = application.applicationState
    if (state == .Inactive || state == .Background) {
        // go to screen relevant to Notification content
    } else {
        // App is in UIApplicationStateActive (running in foreground)
    }
}
Ghulam Rasool
  • 3,996
  • 2
  • 27
  • 40
  • 2
    That was not the question. The problem is how to recognize this method being called by the system or by the user action of tapping the notification. – Rivera Jan 10 '17 at 20:13
4

In iOS 10.0+ you can use the method

- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler;

to detect when user taps on a system-displayed alert from the NotificationCenter.

If you implement the method above

  • - (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult result))completionHandler; will be called only when a notification is received
  • - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler; will be called when user taps on notification
Giorgio
  • 1,973
  • 4
  • 36
  • 51
3

When application: didReceiveRemoteNotification:fetchCompletionHandler: method is called app state is UIApplicationStateInactive if user taps on alert (in this case you would like to prepare some UI) and is UIApplicationStateBackground when app is silently woken (in this case you just load some data).

Alexey Kozhevnikov
  • 4,249
  • 1
  • 21
  • 29
  • 8
    In the case of iOS 10+ the notification can be shown while the app is open. In this case both the system and the user tap will call `application:didReceiveRemoteNotification:fetchCompletionHandler:` and both will have `application.applicationState == .active` which is a problem. – Rivera Jan 10 '17 at 20:19
2

I'm not sure if I understand your question.

Do you want to differentiate between a silent push notification background fetch and a noisy push notification? You can simply check whether the push notification dictionary contains the "content-available" key: [[userInfo objectForKey:@"aps"] objectForKey:@"content-available"] If it does, then it should be a silent push. If not, it was a normal push.

Do you want to know if that background fetch method is called when the application receives a notification and it is in suspended/not running? If so, you can do the following:

  • Download and import LumberJacks into your app. Look through the directions and learn how to set it up such that you can save logs to the disk.
  • Put this in any method you want to see whether/when that method is invoked:

    • DDLogDebug(@"%@ - %@",NSStringFromSelector(_cmd),NSStringFromClass([self class]));

    This will print the class and the method to the log file.

  • Examine the log file after sending yourself a push notification to your background-fetch enabled app, and see if any of the methods get called by looking at your log file.

If you have set up your app correctly for background fetch, the method application: didReceiveRemoteNotification: fetchCompletionHandler: will be called even when the app is backgrounded/not running if you receive a push notification (silent push or not).

navkast
  • 466
  • 2
  • 13
0

For Swift: In application(_:didFinishLaunchingWithOptions:) parse the application options. If they exist, you know the app was launched from them tapping.

  if let remoteNotif = launchOptions?[UIApplicationLaunchOptionsKey.remoteNotification] as? [String: Any] {
        print("Remote notfi is \(remoteNotif)")
        if let notification = remoteNotif["aps"] as? [AnyHashable : Any] {
        /// - parse notification
      }
  }

Otherwise, you can handle the tap in, and you know that the app is open/background/inactiveapplication(_:didReceiveRemoteNotification:fetchCompletionHandler:)

Alex Mason
  • 385
  • 1
  • 5
  • 11