YogevSitton
YogevSitton

Reputation: 10108

didReceiveRemoteNotification not working in the background

I'm working on a big app with a huge chunk of legacy code. Currently - there's an implementation for:

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

The problem is that it is only called when the app is in the foreground OR when the user taps the the notification while the app is in the background. I tried to implement:

- (void) application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler

But the app behaves the same. In any case - this method is not called when the app is in the background. What could be the problem?

Upvotes: 47

Views: 47172

Answers (9)

ambassador
ambassador

Reputation: 444

test it on a real device!!

After spending hours trying all the answers posted here I decided to test it on a real device (it didn't work on the simulator) and it finally worked!

Upvotes: 1

WilliamX
WilliamX

Reputation: 447

There is another scenario that most of the replies are not identifying and the docs are not mentioning either.

In my case, I had my device configured with a special uncommon/unrelated setting.

Go to Settings -> General -> Background App Refresh

You are presented with 3 options:

  1. Off
  2. Wi-Fi
  3. Wi-Fi Cellular Data

!IMPORTANT! There are a viral videos on social network recommending setting this to "Off" or "Wi-Fi" in order to save battery life / data plan. Thats when I set this to "OFF", and didReceiveRemoteNotification was never being called in the background, but strangely enough it was called once I opened the app.

Even if you have it on "Wi-Fi / Cellular Data", if you don't have internet, it wont be called (duhh), but most important, I think the guides should recommend having this small detail covered in case there are users that fall in the "condition" of having this set to "Off" or "Wi-Fi" and being on Cellular network <- this is more common case.

The conditional for this can be retrieved at: UIApplication.shared.backgroundRefreshStatus

https://developer.apple.com/documentation/uikit/uiapplication/1622994-backgroundrefreshstatus

And the funny thing is that it will mess up your logic once the device is in low-power mode, because it will automatically change the status to "Off".

There is nowhere to find that this impacts notifications arriving when app is in background mode, so thats odd. Anyways, hope this helps -roberto

Upvotes: 1

Mahdi Moqadasi
Mahdi Moqadasi

Reputation: 2479

I fix this by adding two methods one for tap and one for foreground in appDelegate:

func application(_ application: UIApplication,
                 didReceiveRemoteNotification userInfo: [AnyHashable: Any],
                 fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult)
                 -> Void) {
    print(">>>NOTIF RECEIVED: ACTION:\(userInfo["action"] ?? "nil") DATA: \(userInfo["data"] ?? "nil")<<<")
    completionHandler(UIBackgroundFetchResult.newData)
}
    
func userNotificationCenter(_ center: UNUserNotificationCenter,
                            didReceive response: UNNotificationResponse,
                            withCompletionHandler completionHandler: @escaping () -> Void) {
    let userInfo = response.notification.request.content.userInfo
    print(">>>TAP NOTIF RECEIVED: ACTION:\(userInfo["action"] ?? "nil") DATA: \(userInfo["data"] ?? "nil")<<<")
    completionHandler()
}

Important Prerequisites:

  • inyourTarget/Signing&Capabilities add background modes and check remote notifications

  • check that initialize done before, in appDelegate/didfinishLaunchingWithOptions like this:

      FirebaseApp.configure()
    
      UNUserNotificationCenter.current().delegate = self
      Messaging.messaging().delegate = self
      let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]
      UNUserNotificationCenter.current().requestAuthorization(
          options: authOptions,
          completionHandler: { _, _ in }
      )
    
      application.registerForRemoteNotifications()
    

Upvotes: 0

ArunGJ
ArunGJ

Reputation: 2683

My device was in a bad state. I had to restart the device to get it working although I had done all the pre-requisites mentioned here.

Upvotes: 1

Lance Samaria
Lance Samaria

Reputation: 19572

I'm using iOS 14 and I did everything from the accepted answer but nothing. I needed the userInfo from didReceiveRemoteNotification but it would never get called from the background even when I would press the actual notification. I had to use the below method and inside that method I use UIApplication.shared.applicationState to check for the background state. I get the userInfo from there.

// called when notification is tapped
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {

    let userInfo = response.notification.request.content.userInfo

    if let dict = userInfo as? [String: Any] {

        let state = UIApplication.shared.applicationState
        if state == .inactive || state == .background {
                
                print("app is in BACKGROUND - userInfo: ", dict) // do something with dict when app is in the background and user taps the notification
                
        } else {
                
                print("app is in FOREGROUND - userInfo: ", dict) // do something with dict when app is in the foreground and user taps the notification
        }
    }

    completionHandler()
}

Assuming you have everything else setup correctly, when you tap the notification while the app is in the background, you will get the correct print statement with the userInfo.

Upvotes: -1

Baris Akar
Baris Akar

Reputation: 4965

Implementing didReceiveRemoteNotification and didReceiveRemoteNotification:fetchCompletionHandler is the correct way, but you also need to do the following:

Make sure to register for remote notifications, see documentation here:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{    
    [[UIApplication sharedApplication] registerForRemoteNotificationTypes:(UIRemoteNotificationTypeAlert | UIRemoteNotificationTypeBadge | UIRemoteNotificationTypeSound)];

    return YES;
}

Also make sure to edit Info.plist and check the "Enable Background Modes" and "Remote notifications" check boxes:

enter image description here

Additionally, you need to add "content-available":1 to your push notification payload, otherwise the app won't be woken if it's in the background (see documentation here updated):

For a push notification to trigger a download operation, the notification’s payload must include the content-available key with its value set to 1. When that key is present, the system wakes the app in the background (or launches it into the background) and calls the app delegate’s application:didReceiveRemoteNotification:fetchCompletionHandler: method. Your implementation of that method should download the relevant content and integrate it into your app

So payload should at least look like this:

{
    aps = {
        "content-available" : 1,
        sound : ""
    };
}

Upvotes: 85

AndyW58
AndyW58

Reputation: 345

I am creating an iOS project that will use Firebase Cloud Messaging (FCM) to deliver custom data elements, sent via Firebase/APNS notifications, from a company server to the iOS device.

The first thing I had to understand is that unlike Android, there is no similar type of 'Service' that will be able to capture and save information I'm sending, regardless if the app is in the foreground (active), background (still in memory) or not active (not in memory). Therefore, I have to use Notification messages NOT Data messages like I had designed for Android.

After much reading to understand both Apple APNS and the Firebase interface between the iOS app and APNS server, looking at countless posts on stackoverflow and other web resources, I finally figured out how to get this to work for my requirements.

When a Firebase Cloud Messaging (FCM) Notification message is sent from the server (or Firebase Console as the FCM defaults to Notification NOT Data messages), it is delivered via APNS and presented as a notification on the iOS device. When the user taps on the notification banner, iOS does the following: if the app is not running/loaded iOS launches the app, if the app is loaded/running but in the background iOS brings the app to the foreground OR if the app is in the foreground (all three cases), the message content is then delivered via func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: (UIBackgroundFetchResult) -> Void) {}.

One thing for sure, you Must Enable Background Modes and check Remote Notification, you DO NOT have to include {"content-available" : 1} in the payload.

1) Go through the APNS and Firebase setup, pretty straight forward, generate and register certificates and such.

2) In appDelegate, didFinishLaunchingWithOptions, add:

        Messaging.messaging().delegate = self as? MessagingDelegate

        if #available(iOS 10.0, *) {

            UNUserNotificationCenter.current().delegate = self

            let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]

            UNUserNotificationCenter.current().requestAuthorization(
                options: authOptions,
                completionHandler: {_, _ in })
        }
        else {

            let settings: UIUserNotificationSettings =
                UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)

            application.registerUserNotificationSettings(settings)
        }

        application.registerForRemoteNotifications()

3) Then add these call back functions to appDelegate:

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

        // Print message ID.
        if let messageID = userInfo["gcm.message_id"] {

            print("\n*** application - didReceiveRemoteNotification - fetchCompletionHandler - Message ID: \(messageID)")
        }

        // Print full message.
        print("\n*** application - didReceiveRemoteNotification - full message - fetchCompletionHandler, userInfo: \(userInfo)")

        myNotificationService?.processMessage(title: userInfo["Title"] as! String
            , text: userInfo["Text"] as! String, completion: { (success) in

                if success {
                    completionHandler(.newData)
                }
                else {
                    completionHandler(.noData)
                }
        })
    }

    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {

        completionHandler([.alert, .badge, .sound])
    }

Very Helpful:

https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/CreatingtheNotificationPayload.html

https://firebase.googleblog.com/2017/01/debugging-firebase-cloud-messaging-on.html

Upvotes: 3

Mahadev Mandale
Mahadev Mandale

Reputation: 447

  1. Register for push notification in app delegate.
  2. Add background mode in app capabilities.
  3. Add "content-available"="1" while sending the push notification(if you are using firebase replace "content-available"="1" by "content_available"="true" while sending the push notification from server side).

Upvotes: 17

ZaEeM ZaFaR
ZaEeM ZaFaR

Reputation: 1536

I had the same problem. Notification banner appeared, but -application:didReceiveRemoteNotification:fetchCompletionHandler: method was not called. The solution for me that worked was to add implementation of - application:didReceiveRemoteNotification: method and forward call to -application:didReceiveRemoteNotification:fetchCompletionHandler::

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

    self.application(application, didReceiveRemoteNotification: userInfo) { (UIBackgroundFetchResult) in

    }
}

Upvotes: 8

Related Questions