Firebase IOS Flutter

How to Fix ‘onMessage’ Event Not Firing in Foreground on iOS Flutter

If you’re working with Firebase Cloud Messaging (FCM) in a Flutter app, you may encounter an issue where the onMessage event doesn’t trigger when your app is in the foreground on iOS. This problem arises because iOS apps do not automatically display notifications when they are active. As a result, the onMessage event might not be fired as expected.

In this article, we’ll guide you through fixing this issue by modifying your AppDelegate.swift file to ensure that notifications are handled properly, even when the app is running in the foreground.

The Issue

The main reason why onMessage isn’t fired in the foreground on iOS is due to Firebase only handling notifications directly sent through Firebase’s own push services. If your app receives notifications from other services or you need custom behavior when receiving a notification (e.g., showing a custom alert or performing background tasks), you will need to intercept the notification manually on iOS.

iOS prevents apps from showing notifications in the foreground by default, expecting the app to handle these events manually. This means that if you rely on onMessage to trigger certain actions, the default Firebase behavior won’t be enough. To resolve this, we can use native code in AppDelegate.swift to invoke the onMessage event manually when a notification is received.

The Solution: Modify AppDelegate.swift

1. Open AppDelegate.swift

Navigate to your project’s ios/Runner folder and open the AppDelegate.swift file. This file contains the code that manages the lifecycle of your iOS app, including handling notifications.

2. Add Firebase and Messaging Imports

Ensure the necessary Firebase imports are present in your AppDelegate.swift file:

import UIKit
import Flutter
import Firebase
import FirebaseMessaging

3. Manually Handle Notifications in Foreground

To trigger the onMessage event when the app is in the foreground, override the application(_:didReceiveRemoteNotification:fetchCompletionHandler:) method. This method will capture incoming notifications and pass them to Flutter using a method channel.

Here is the complete implementation:

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    
    override func application(
        _ application: UIApplication,
        didReceiveRemoteNotification userInfo: [AnyHashable : Any],
        fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
    ) {
        // Get the root view controller (Flutter view)
        let controller: FlutterViewController = window?.rootViewController as! FlutterViewController
        
        // Define the method channel that communicates between Flutter and native iOS code
        let channel = FlutterMethodChannel(name: "plugins.flutter.io/firebase_messaging",
                                           binaryMessenger: controller.binaryMessenger)
        
        // Convert the userInfo dictionary into the required format for Flutter
        let dictionary: NSDictionary = remoteMessageUserInfoToDict(userInfo: userInfo)
        
        // Trigger the "onMessage" event in Flutter
        channel.invokeMethod("Messaging#onMessage", arguments: dictionary)
        
        // Call the completion handler to notify iOS that the fetch is done
        completionHandler(.newData)
    }
    
    // Helper function to format remote message payload into a Flutter-friendly dictionary
    func remoteMessageUserInfoToDict(userInfo: [AnyHashable: Any]) -> NSDictionary {
        var message = [String: Any]()
        var data = [String: Any]()
        var notification = [String: Any]()
        var notificationIOS = [String: Any]()

        // Loop through the notification data
        for (key, value) in userInfo {
            guard let keyString = key as? String else { continue }

            // Handle standard FCM fields
            switch keyString {
                case "gcm.message_id", "google.message_id", "message_id":
                    message["messageId"] = value
                case "message_type":
                    message["messageType"] = value
                case "collapse_key":
                    message["collapseKey"] = value
                case "from":
                    message["from"] = value
                case "google.c.a.ts":
                    message["sentTime"] = value
                case "to", "google.to":
                    message["to"] = value
                case "fcm_options":
                    if let options = value as? [String: Any], let image = options["image"] as? String {
                        notificationIOS["imageUrl"] = image
                    }
                default:
                    if keyString.hasPrefix("gcm.") || keyString.hasPrefix("google.") || keyString == "aps" {
                        continue
                    }
                    data[keyString] = value
            }
        }

        // Add data to the message
        message["data"] = data

        // Extract and handle iOS notification payload (aps)
        if let apsDict = userInfo["aps"] as? [String: Any] {
            if let category = apsDict["category"] as? String {
                message["category"] = category
            }
            if let threadId = apsDict["thread-id"] as? String {
                message["threadId"] = threadId
            }
            if let contentAvailable = apsDict["content-available"] as? Bool {
                message["contentAvailable"] = contentAvailable
            }
            if let mutableContent = apsDict["mutable-content"] as? Int, mutableContent == 1 {
                message["mutableContent"] = true
            }

            // Handle iOS notification-specific fields
            if let alertDict = apsDict["alert"] as? [String: Any] {
                if let title = alertDict["title"] as? String {
                    notification["title"] = title
                }
                if let body = alertDict["body"] as? String {
                    notification["body"] = body
                }
                // Handle iOS-specific notification fields
                if let subtitle = alertDict["subtitle"] as? String {
                    notificationIOS["subtitle"] = subtitle
                }
                if let badge = apsDict["badge"] as? Int {
                    notificationIOS["badge"] = String(badge)
                }
            }

            notification["apple"] = notificationIOS
            message["notification"] = notification
        }

        // Return the formatted message dictionary
        return message as NSDictionary
    }
}

Explanation of the Code:

  • application(_:didReceiveRemoteNotification:fetchCompletionHandler:): This method is triggered when your app receives a remote notification. It captures the notification payload and invokes the Flutter onMessage event through a method channel.
  • remoteMessageUserInfoToDict(userInfo:): This function converts the notification payload (userInfo) into a format that the Flutter app can process.
  • Flutter Method Channel: The method channel allows communication between the native iOS code and the Flutter code. It invokes the Messaging#onMessage method in Flutter with the converted notification data.

4. Handle Foreground Notifications in Flutter

Now, in your Dart code, you can handle the onMessage event when the app is in the foreground:

FirebaseMessaging.onMessage.listen((RemoteMessage message) {
  // Handle the message when the app is in the foreground
  print('Foreground message: ${message.data}');
  // Perform any additional handling here, like showing a custom notification
});

Conclusion

By modifying AppDelegate.swift to manually handle notifications in the foreground, you can ensure that all notifications, including those not sent directly from Firebase, trigger the onMessage event in your Flutter app. This fix provides more control over your app’s notification handling and improves its responsiveness to real-time messages.

If you’re building a Flutter app with custom notification handling, this solution will help you ensure that notifications are processed regardless of the app’s state.