3

I am using FCM to create and send push notifications for my iOS app.

Dev Environment:

  • Xcode 11.3

  • iPhone X running iOS 13.3

  • Swift 5.2

Pod Versions:

  • Firebase 6.14.0
  • FirebaseMessaging 4.1.10
  • FirebaseInstanceID 4.2.8

Problem:

Prior to running into an issue, I had setup my app to be able to receive notifications when the app was in both the background and the foreground. Very happy with myself I committed the code. After this point I have been unable to receive notifications in the foreground OR the background. Regardless of using whether the notification is sent from Cloud Messaging dashboard or POSTMAN, I receive a successful response but the notification never appears.

At first I thought I may have hit the notification quota but it is now 2 days post fact.

To troubleshoot I have tried:

  1. Uninstalled and re-installed the app (Which refreshes the device token)
  2. Moved UIApplication.shared.registerForRemoteNotifications() to before FirebaseApp.configure()
  3. Downloaded a fresh GoogleService-Info.plist and replaced existing
  4. Checked that bundle id's etc all match
  5. Updated firebase pods to latest (FirebaseMessaging was at 4.1.9 if that helps)
  6. Set Messaging.messaging().shouldEstablishDirectChannel = true
  7. Removed and Re-added the needed capabilities
  8. Set FirebaseAppDelegateProxyEnabled to both YES and NO
  9. Set shouldEstablishDirectChannel = true
  10. Set useMessagingDelegateForDirectChannel = true
  11. Moved some logic from didFinishLaunchingWithOptions() to applicationDidBecomeActive()

Code:

Note: This is the unaltered code that originally worked for me.

AppDelegate.swift

import UIKit
import Firebase
import FBSDKCoreKit
import GoogleMaps
import SwiftLocation
import GooglePlaces
import Crashlytics
import GoogleSignIn
import Armchair
import UserNotifications
import FirebaseMessaging

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {

    var window: UIWindow?
    var swipeNavigationViewController: SwipeNavigationViewController!

    override init() {
        super.init()

        FirebaseApp.configure()

        Database.database().isPersistenceEnabled = true
        swipeNavigationViewController = SwipeNavigationViewController()
    }

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool  {
        FirebaseConfiguration.shared.setLoggerLevel(.error)
        ApplicationDelegate.shared.application(application, didFinishLaunchingWithOptions: launchOptions)

        // Google Maps
        GMSServices.provideAPIKey(FireBaseConstants.GoogleAPIKey)
        GMSPlacesClient.provideAPIKey(FireBaseConstants.GoogleAPIKey)
        GeocoderRequest.GoogleOptions(APIKey: FireBaseConstants.GoogleAPIKey)

        let navigationViewController = UINavigationController(rootViewController: swipeNavigationViewController)
        navigationViewController.setNavigationBarHidden(true, animated: false)

        self.window?.rootViewController = navigationViewController
        self.window?.makeKeyAndVisible()

        showAlertIfPointedTowardProductionDatabase()
        setupReviewRequest()

        UIApplication.shared.registerForRemoteNotifications()

        let center = UNUserNotificationCenter.current()
        center.requestAuthorization(options:[.badge, .alert, .sound]) { (granted, error) in
            // If granted comes true you can enabled features based on authorization.
            guard granted else { return }
            DispatchQueue.main.async {
                print("UserID: \(UserManager.sharedManager.currentUser?.userID)")
                let pushManager = PushNotificationManager(userID: "currently_logged_in_user_id")
                pushManager.registerForPushNotifications()
            }
        }

        UNUserNotificationCenter.current().delegate = self

        return true
    }

    func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
        let handledByFB = ApplicationDelegate.shared.application(app, open: url, options: options)

        var handledByGoogle = false
        if !handledByFB {
            handledByGoogle = GIDSignIn.sharedInstance().handle(url)
        }

        let handled = handledByFB || handledByGoogle

        return handled
    }

    private func setupReviewRequest() {
        //Code...
    }

    // This method will be called when app received push notifications in foreground
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.alert, .badge, .sound])
    }
}

PushNotificationManager.swift

import Foundation
import Firebase
import FirebaseFirestore
import FirebaseMessaging
import UIKit
import UserNotifications

class PushNotificationManager: NSObject, MessagingDelegate, UNUserNotificationCenterDelegate {

    let userID: String
    let gcmMessageIDKey = "gcm.message_id"

    init(userID: String) {
        self.userID = userID
        super.init()
    }

    func registerForPushNotifications() {
        let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound]

        UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { (_, error) in
            guard error == nil else{
                print(error!.localizedDescription)
                return
            }
        }

        //get application instance ID
        InstanceID.instanceID().instanceID { (result, error) in
            if let error = error {
                print("Error fetching remote instance ID: \(error)")
            } else if let result = result {
                print("Remote instance ID token: \(result.token)")
            }
        }

        UIApplication.shared.registerForRemoteNotifications()
        updateFirestorePushTokenIfNeeded()
    }

    func updateFirestorePushTokenIfNeeded() {
        if let token = Messaging.messaging().fcmToken {
            //            let usersRef = Firestore.firestore().collection("users_table").document(userID)
            //            usersRef.setData(["fcmToken": token], merge: true)
            print("Remote instance ID token: \(token)")
        }
    }

    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String) {
        print("Firebase registration token: \(fcmToken)")

        let dataDict:[String: String] = ["token": fcmToken]
        NotificationCenter.default.post(name: Notification.Name("FCMToken"), object: nil, userInfo: dataDict)
        // TODO: If necessary send token to application server.
        // Note: This callback is fired at each app startup and whenever a new token is generated.
    }

    func messaging(_ messaging: Messaging, didReceive remoteMessage: MessagingRemoteMessage) {
        print("Received data message: \(remoteMessage.appData)")
    }

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

    func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any]) {
        if let messageID = userInfo[gcmMessageIDKey] {
            print("Message ID: \(messageID)")
        }

        print(userInfo)
    }

    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("Unable to register for remote notifications: \(error.localizedDescription)")
    }

    func application(_ application: UIApplication,didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        let tokenParts = deviceToken.map { //data -> String in
            return String(format: "%02.2hhx", $0)
        }

        Messaging.messaging().apnsToken = deviceToken
        Messaging.messaging().setAPNSToken(deviceToken, type: .unknown)
        UserDefaults.standard.synchronize()
    }
}

This was setup using all of the following links (With a few others I forgot as well I'm sure):

Response Info:

Postman:

{
    "multicast_id": 2586780331808083728,
    "success": 1,
    "failure": 0,
    "canonical_ids": 0,
    "results": [
        {
            "message_id": "0:1578532253832479%2b1845e62b1845e6"
        }
    ]
}

Cloud Messaging:

FCM Console

A Israfil
  • 519
  • 4
  • 25

2 Answers2

2

I was able to resolve the issue by moving

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data)

from PushNotificationManager to AppDelegate. Hope this helps someone else!

A Israfil
  • 519
  • 4
  • 25
0

You can have didRegisterForRemoteNotifications in your controller layer. I have a private method in my controller that calls registerForRemoteNotifications as well. I instantiate my controller in AppDelegate so it's available right away, and I did have an issue when I tried to make the controller without a strong reference, probably having to do with weak delegate(s).

This is my entire appDelegate:

import UIKit
import Firebase

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    var controller: FirebaseMessagingController!
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        FirebaseApp.configure()
        
        self.controller = FirebaseMessagingController.shared
        return true
    }

    // MARK: UISceneSession Lifecycle

    func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        // Called when a new scene session is being created.
        // Use this method to select a configuration to create the new scene with.
        return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
    }

    func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // Called when the user discards a scene session.
        // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
        // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
    }


}

My controller's init:

    private init() {
        NotificationCenter.default.addObserver(
            self,
            selector: #selector(receiveToken(_:)),
            name: .tokenKey,
            object: nil)
        
        registerForRemoteNotifications(UIApplication.shared)
        requestNotificationPermissions { _ in } // TODO: move to more user friendly place
        Messaging.messaging().delegate = UIApplication.shared.delegate as? MessagingDelegate
        UNUserNotificationCenter.current().delegate = UIApplication.shared.delegate as? AppDelegate
    }
    
    @objc private func receiveToken(_ notification: Notification) {
        
        guard let tokenDict = notification.userInfo as? [Notification.Name: String],
              let token = tokenDict[.tokenKey] else { return }
        self.token = token
        let apiTokenDict = ["token": token]
        if AuthService.shared.isLoggedIn {
            guard let user = AuthService.shared.user else { return }
            FirebaseDatabaseController().updateValues(for: APIRef.userRef(userId: user.userId).endpoint, with: apiTokenDict)
        }
        
    }
    
    private func registerForRemoteNotifications(_ application: UIApplication) {
        application.registerForRemoteNotifications()
    }
    
    private func requestNotificationPermissions(completion: @escaping (Result<Bool, Error>) -> Void) {
        
        let authOptions: UNAuthorizationOptions = [.alert, .badge]
        UNUserNotificationCenter.current().requestAuthorization(
            options: authOptions,
            completionHandler: { success, error in
                if success {
                    completion(.success(success))
                } else if let error = error {
                    completion(.failure(error))
                } else {
                    let error = NSError(domain: #function, code: 0)
                    completion(.failure(error))
                }
            }
        )
        
    }

didRegisterForRemoteNotifications is also in my controller and receives the token:

    // set FirebaseMessaging service with apnsToken
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        Messaging.messaging().apnsToken = deviceToken
    }

Edit: Actually, I have one more piece in AppDelegate, receiving the registration token, but hid it in an extension in my controller:

extension AppDelegate: MessagingDelegate {
    
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
        guard let fcmToken = fcmToken else { return }
        let dataDict:[NSNotification.Name: String] = [.tokenKey: fcmToken]
        NotificationCenter.default.post(name: .tokenKey, object: nil, userInfo: dataDict)
    }
    
}

Thinking about it, it's a weird choice since I could probably make the controller the messaging delegate and do it there without posting a notification....

froggomad
  • 1,747
  • 2
  • 17
  • 40