- [ between the brackets ]
- Posts
- Device-to-Device Notifications on iOS
Device-to-Device Notifications on iOS
How to set up d2d notifications with Firebase
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:
Go to the Cloud Messaging tab in your Firebase project settings
Under Project Credentials, generate a new private key which will be used by the server to authenticate FCM requests
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 ]