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 FlutteronMessage
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.