launchOptions always nil when launching from a push notification
Asked Answered
S

3

13

I'm sending push notifications from a Django app (using django-push-notifications) to an iOS app. The app targets iOS 13 and I'm running it on an iPhone 7 running iOS 13.3.1. I'm debugging in Xcode 11.3.1

I'm trying two different methods to send the notification from the Django side:

Method 1:

devices.send_message(message={"title" : title, "body" : message}, thread_id="events", extra={"foo": "bar"})

Method 2:

devices.send_message("[will be overwritten]", extra={
    "aps": {
        "alert": {
            "title": "Bold text in the notification",
            "body": "Second line in the notification"
        },
        "sound": "default",
    },
    "foo": "bar"
})

As far as I can tell, both methods should result in a payload which looks like Method 2.

I'm debugging by doing the following:

  1. Set "wait for executable to be launched" in my device scheme
  2. Build and run in Xcode
  3. Ensure app has been killed in the task switcher
  4. Trigger sending of remote notification
  5. Tap on received notification to launch app

No matter what I do, launchOptions is always nil. I've tried setting a breakpoint to inspect the variables. I've tried using os_log to log to the console if launchOptions is not nil, and I've tried triggering an alert (following advice from this question) to rule out Xcode debugger interference. It's always nil.

My AppDelegate currently looks like this:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

    let notificationOption = launchOptions?[.remoteNotification]

    let alert = UIAlertController(title: "Your title", message: notificationOption.debugDescription, preferredStyle: .alert)
    let cancel = UIAlertAction(title: "Cancel", style: .default, handler: { action in
    })
    alert.addAction(cancel)
    DispatchQueue.main.async(execute: {
        application.windows.first!.rootViewController?.present(alert, animated: true, completion: nil)

    })
    return true
}

The alert triggers, but the alert content simply reads "nil".

I can't figure out what's missing. It's possible that my notification payload isn't exactly what I think it is (I've asked on the Github page for django-push-notifications to confirm if there's an issue on that end). It's also possible I've missed a step in setting up remote notifications, but I do reliably receive the notifications and they display as I expect, so they seem to be working.

Any advice greatly appreciated!

Sundew answered 31/1, 2020 at 16:26 Comment(4)
How did you setup the project so we can try to help by reproducing your steps?Lunette
I'm not sure I know what you're asking for. The iOS app is set up using the standard "Tabbed App" template in Xcode. It uses SwiftUI for UI and Alamofire for HTTP requests, other than that it just uses standard iOS SDK features. The AppDelegate is set up in the way I shared. The backend is Django 2.2.9 with Django-push-notifications handling APNS calls, using the API call I shared. I'm using dev certificates for APNS and running the server locally. I trigger the sending of the notification from my web app.Sundew
I don't know if this applies to your particular situation or not since I was not working with Notifications specifically, but I was working on a very similar problem with an app I was launching from an associated file type targeting iOS 13 and what I finally figured out is that a lot of things were moved from the AppDelegate to the SceneDelegate in iOS 13. So you might do some research and see if that is the case for you. Most of the example code you find online uses the AppDelegate because they were written before iOS 13, which was really confusing for me.Ockeghem
Thanks @vikingmobile, I was beginning to suspect that Apple might have changed something and not told anyone (and since few people are targeting iOS 13, not many would have noticed yet). I found a workaround (see below), but if the workaround causes issues, I'll try SceneDelegate.Sundew
T
30

In iOS 13.0 and above When the app is killed, if you tap on notification, would like to open the app and get hold of notification payload. Here is how you do it.

Please check for connectOptions under sceneDelegate

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
//look for remote notification response
   if let response = connectionOptions.notificationResponse{
        print(response.notification.request.content.userInfo)
   }

   guard let _ = (scene as? UIWindowScene) else { return }
} 
Tuneless answered 4/9, 2020 at 8:54 Comment(3)
So glad I found this answer. Helped me greatly. Thank you.Desman
This also happens for iOS 14.*Floydflss
Worth noting (for anyone who battled similar issues as me) - if testing this using simulators, often this method in SceneDelegate will not reliably hit breakpoints when launching the app from a notification (even though the app launches apparently normally when you tap on the simulated notification). Best to use a real device (as is often the case).Freida
S
6

I didn't find a solution to this issue, but I found a workaround. I still have no idea why launchOptions was always nil, but I've been able to access the payload by doing the following:

In AppDelegate.swift:

class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {

...

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        UNUserNotificationCenter.current().delegate = self
        return true
    }

...

    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        let actionIdentifier = response.actionIdentifier

        switch actionIdentifier {
        case UNNotificationDismissActionIdentifier: // Notification was dismissed by user
            // Do something
            completionHandler()
        case UNNotificationDefaultActionIdentifier: // App was opened from notification
            // Do something
            completionHandler()
        default:
            completionHandler()
        }
    }

If I then set a breakpoint in userNotificationCenter, I can dig out the notification payload: Debugger screen shot (breakpoint)

Sundew answered 3/2, 2020 at 21:32 Comment(2)
You can will definitely get a call for userNotificationCenter(_:didReceive:withCompletionHandler , but that has been available since iOS10, If what you're saying is correct then why does Apple still offer remotenotification as a launch option?Appointment
But what if you need to know if your app has been initial launching from notifications or not?Redcap
T
5

It seems that after changes in iOS 13 we don't have to process notifications in didFinishLaunchingWithOptions function.

We can just use:

extension AppDelegate: UNUserNotificationCenterDelegate{
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {

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

        if let aps = userInfo["aps"] as? [String: AnyObject] {
            // Do what you want with the notification
        }

      completionHandler()
    }
}

It works for any scenario when the user clicks on a notification.

Torpor answered 25/3, 2020 at 20:47 Comment(8)
You can will definitely get a call for userNotificationCenter(_:didReceive:withCompletionHandler , but that has been available since iOS10, If what you're saying is correct then why does Apple still offer remotenotification as a launch option? Additionally how is your answer different from the other answer that was given a month ago?Appointment
Not sure if it's a bug or a feature, but it seems that when you use config with SceneDelegate (>iOS 13) then launch options are nil when your app has been initial launching from notifications.Torpor
I just don't know that. I was giving you information based on docs. Just to be crystal clear hear, are you saying that you did get the launchOptions before iOS 13? Or that's just a guess?Appointment
I can confirm that launch options is NOT nil before iOS 13Redcap
Yup, before iOS 13 launch options are not nil. There is nothing in official docs about this but after switching to iOS 13 and configure SceneDelegate the only way to get information about opening app from the notification is using UNUserNotificationCenterDelegate. Maybe it's only a bug which will be fixed soon, but it's nice to know. I spent the whole day looking for the solution so maybe my answer will be helpful for somebody :)Torpor
@Torpor So, for now there is no way to know if the app was launched from a notification right? (iOS 13)Redcap
@Redcap the func userNotificationCenter in the above code solves the issue. See the below @Spielo's answer for details.Something
Still in iOS 15, I see UNUserNotificationCenter is more reliable when it comes to get the notification info than in SceneDelegateCotopaxi

© 2022 - 2024 — McMap. All rights reserved.