Device-to-Device Notifications on iOS

How to set up d2d notifications with Firebase

If you aren’t already taking weekly programming deep dives with me, subscribe below!

Setting Up Device-to-Device Notifications with Firebase

Firebase Cloud Messaging (FCM) makes it easy to send notifications between devices. With just a few lines of code, you can build cross-platform notification functionality into your iOS apps. In this guide, I’ll show you how to set up device-to-device notifications using Firebase with example JavaScript and Swift code.

Prerequisites

Before getting started, make sure you have:

  • The Firebase SDK installed in your JavaScript and Swift projects

  • A Firebase project set up in the Firebase console

  • The Cloud Messaging and Analytics services enabled for your project

  • Firebase Firestore enabled for storing all communication

Configuration

In the Firebase console, you'll need to generate a server key and sender ID:

  1. Go to the Cloud Messaging tab in your Firebase project settings

  2. Under Project Credentials, generate a new private key which will be used by the server to authenticate FCM requests

  3. Take note of the Sender ID - this identifies your app server that sends messages

With the server key and Sender ID, you're ready to start sending notifications between devices.

Server

First, create a simple Firebase Function that triggers whenever a new entry appears in the messages collection of your Firestore.

Add the following code to your index.js file:

const functions = require("firebase-functions");
const admin = require("firebase-admin");
const { onDocumentCreated } = require("firebase-functions/v2/firestore");

admin.initializeApp();

const messagePath = "messages/{messageId}";
exports.sendNotification = onDocumentCreated(messagePath, async (event) => {
    functions.logger.log("New message document was created");

    const snapshot = event.data;
    if (!snapshot) {
        console.log("No data associated with the event");
        return;
    }
    const data = snapshot.data();
	const title = data.title;
    const text = data.text;
    const sender = data.sender;

    // Listing all tokens as an array.
    const tokens = data.tokens;

    // Send message to all tokens
    const response = await admin.messaging().sendEachForMulticast({
        tokens: tokens,
        notification: {
            title: title,
            body: text,
        },
        data: {
            sender: sender,
            time: data.time,
        },
    });

    functions.logger.log("Successfully sent Notification");

    // For each message check if there was an error.
    const tokensToRemove = [];
    response.responses.forEach((result, index) => {
        const error = result.error;
        if (error) {
            functions.logger.error(
                'Failure sending notification to',
                tokens[index],
                error
            );
            // Cleanup the tokens who are not registered anymore.
            if (error.code === 'messaging/unregistered' || 
                error.code === 'messaging/invalid-argument') {
                tokensToRemove.push(tokens.splice(index, 1));
            }
        }
    });
    // Update the tokens in the database if any were removed
    if (tokensToRemove.length > 0) {
        await admin.firestore().collection("messages").doc(event.id).update({
            "tokens": tokens
        });
    }
});

Next, you need to ensure your AppDelegate has been configured properly for receiving notifications. Yours should look something like below:

@main
struct ExampleApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate

    let persistenceController = PersistenceController.shared

    var body: some Scene {
        WindowGroup {
            ContentView()
                .environment(\.managedObjectContext, persistenceController.container.viewContext)
                .preferredColorScheme(.light)
        }
    }
}


class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?

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

        // Push Notifications
        if #available(iOS 10.0, *) {
            // For iOS 10 display notification (sent via APNS)
            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()

        Messaging.messaging().delegate = self

        return true
    }
    
    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.
    }


}

extension AppDelegate: UNUserNotificationCenterDelegate {
    func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    willPresent notification: UNNotification,
    withCompletionHandler completionHandler:
    @escaping (UNNotificationPresentationOptions) -> Void
    ) {
        completionHandler([[.banner, .sound]])
    }

    func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse,
    withCompletionHandler completionHandler: @escaping () -> Void
    ) {
        completionHandler()
    }
    
    func application(
      _ application: UIApplication,
      didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
    ) {
        
        let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
        let token = tokenParts.joined()
        print("Device Token: \(token)")

    
    func application(_ application: UIApplication, didReceiveRemoteNotification notification: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
      print("\(#function)")
      if Auth.auth().canHandleNotification(notification) {
        completionHandler(.noData)
        return
      }
    }

    func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool {
      print("\(#function)")
      if Auth.auth().canHandle(url) {
        return true
      }
      return false
    }

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

}

extension AppDelegate: MessagingDelegate {
    func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) {
      print("Firebase registration token: \(String(describing: fcmToken))")

      let dataDict: [String: String] = ["token": fcmToken ?? ""]
      NotificationCenter.default.post(
        name: Notification.Name("FCMToken"),
        object: nil,
        userInfo: dataDict
      )
        



    }
}

And all thats left is the function to “send” the actual message, which will need to include the fcmToken of each user you want to send a message to. Here is a simple implementation:

    func sendMessageToMultipleDevices(text: String, title: String, tokens: [String]) {
        let timestamp = Timestamp(date: Date())
        let date = timestamp.dateValue()
        let dateFormatter = DateFormatter()
        dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" // You can customize the format as needed
        let timestampString = dateFormatter.string(from: date)
        
        do {
            let userId = try self.getUserId()
            
            let messageData: [String: Any] = [
                "title": title,
                "text": text,
                "sender": userId,
                "time": timestampString,
                "tokens": tokens
            ]

            db.collection("messages").addDocument(data: messageData) { error in
                if let error = error {
                    print("Error sending message:", error)
                } else {
                    print("Message sent successfully.")
                }
            }

        } catch {
            print("failed to get user id: \(error)")
        }


    }

This function stores your message in a Firestore collection, triggering the backend Firebase Function created earlier.

And thats all there is to it! A straight forward and cheap approach to sending notifications between devices with Firebase.

[ Zach Coriarty ]