Setting iOS App Badge using in React-Native and User Defaults
Few months ago, KICKGOING released a new feature ‘Notification List’.
We have been sending notifications to users as a feature or for events.
Users would click on the notifications, while others would dismiss ignore it.
Some of them may want to review it.
However our app lacked a feature to display previous notifications to users.
Therefore, olulo UX Team designed ’Notification List’ this time.
Indeed, they wished to have control over the app badge.
What is App Badge?
The App Badge is the number displayed at the top right corner of the App Icon on your phone. It indicates the number of notifications that the app has.
KICKGOING App displays the number 2 as the App Badge, as shown in the screenshot above.
This indicates that the App has 2 unread notifications like this.
Set App Badge in Foreground
The KICKGOING app for Android is already displaying the app badge. The app utilizes the Android operating system’s built-in functionality to synchronize the app badge with the notification count automatically.
However, unlike Android, iOS doesn’t provide such a built-in feature. Therefore, I had to implement this feature manually for iOS.
I was able to set the app badge number using the setApplicationIconBadgeNumber
function of react-native-push-notification when the app is in the foreground.
PushNotification.getApplicationIconBadgeNumber(currentBadge => {
const newBadgeNumber = currentBadge + 1
PushNotification.setApplicationIconBadgeNumber(newBadgeNumber)
})
KICKGOING uses FCM, so we handle the event of receiving a message by using the messaging().onMessage
listener when the app is in the foreground.
On iOS, the background handler does not work for handling message receiving events. In order to handle message receiving events in the background on iOS, I had to use an alternative approach.
Notification Service Extension cannot access badge number of application.
Therefore we need to share share data between Notification service and Application.
To share data, I utilized iOS UserDefaults.
UserDefaults
To access user defaults easily, I created a helper class.
I will this share with Application and Service Extension.
let bundlePrefix = Bundle.main.bundleIdentifier?.replacingOccurrences(of: ".push", with: "") ?? ""
let appGroup = "group.\(bundlePrefix)"
class SharedUserDefaults: NSObject {
private static let defaults = UserDefaults.init(suiteName: appGroup)
private class Keys{
static let badge = "badge"
}
@objc public static var badge: NSNumber? {
get {
guard let value = defaults?.integer(forKey: Keys.badge) else {
return nil
}
return NSNumber.init(value: value)
}
set(value) {
defaults?.setValue(value?.intValue, forKey: Keys.badge)
}
}
}
And I increases ‘badge’ value in the Notification Service Extension. (I am not going to how to create a Service Extension here 😋)
I won’t user defaults if server specified badge.
class NotificationService: UNNotificationServiceExtension {
var content: UNMutableNotificationContent?
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.content = (request.content.mutableCopy() as? UNMutableNotificationContent)
...
self.increaseBadgeNumberIfNeeded(content: content)
}
func increaseBadgeNumberIfNeeded(content: UNMutableNotificationContent?) {
guard let content = content, let applePushInfos = content.userInfo["aps"] as? [AnyHashable : Any] else {
return
}
if let pushBadgeNumber = applePushInfos["badge"] as? Int {
return
}
let currentBadgeNumber = SharedUserDefaults.badge ?? 0
let newBadgeNumber = currentBadgeNumber.intValue + 1
content.badge = NSNumber.init(value: newBadgeNumber)
SharedUserDefaults.badge = NSNumber.init(value: newBadgeNumber)
}
}
How to access badge number with the stored user defaults value in react-native?
I installed react-native-user-defaults to incorporate UserDefaults in React-Native.
To share user defaults, we need to specify an app group.
Therefore, I created an app group with ‘group.{app bundle id}’.
If the value type is a number, I converted it to a string because the user defaults library can’t have number values.
I dynamically imported the react-native-user-defaults library because it is only available for iOS.
const getAppGroup = (): string => {
return `group.${DeviceInfo.getBundleId()}`
}
const setUserDefaults = async (key: string, value: any): Promise<void> => {
const {
default: { set: _setUserDefaults },
} = await import('react-native-user-defaults')
const appGroup = getAppGroup()
let typedValue = value
if (typeof value === 'number') {
typedValue = `${value}`
}
return _setUserDefaults(key, typedValue, appGroup)
}
const getUserDefaults = async (key: string): Promise<any> => {
const {
default: { get: _getUserDefaults },
} = await import('react-native-user-defaults')
return _getUserDefaults(key, getAppGroup())
}
Synchronize
Actually, the Service Extension is for the killed status and only increases the number. If the user removes notifications, the badge number will be incorrect.
Therefore I wrote code to synchronize badge number and store the count of notification receiving messages in the background status.
AppDelegate.swfit
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
...
[[UNUserNotificationCenter currentNotificationCenter] getDeliveredNotificationsWithCompletionHandler:^(NSArray<UNNotification *> * _Nonnull notifications) {
NSNumber *currentCount = [NSNumber numberWithInteger:notifications.count];
NSNumber *newBadgeNumber = [NSNumber numberWithInteger:currentCount.intValue + 1];
application.applicationIconBadgeNumber = newBadgeNumber.integerValue;
SharedUserDefaults.badge = newBadgeNumber;
}];
}
Despite the code mentioned above, the app would still have the wrong badge number.
To address this issue, I implemented a method to synchronize it during the app’s launching process.
import PushNotification from 'react-native-push-notification'
...
static async syncBadge(): Promise<void> {
return new Promise(resolve => {
PushNotification.getDeliveredNotifications(notificiations => {
try {
const notificationCount = notificiations.length
PushNotification.setApplicationIconBadgeNumber(notificationCount)
setUserDefaults('badge', notificationCount)
} finally {
resolve()
}
})
})
}
Originally, I attempted to control the badge number using user defaults.
However, I have now decided to synchronize the number without utilizing user defaults.