iOS Push Notifications: Part 4 — Extensions and Special Modes
Here are some special cases with delivering and handling push notifications.
Background push
Sometimes you need notify your app, perform some background task without notifying user. Background pushes come to help. To enable, in Xcode target → Signing & Capabilities add “Background Modes” capability and enable “Remote notifications”
There is special delegate method of UIApplicationDelegate
which receives background pushes. Even if your app is not running, system will wake it up. App has 30 sec to perform task and return result informing system whether you get any new data or failed.
func application(
_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any]
) async -> UIBackgroundFetchResult {
// perform any task in less than 30 sec
return .newData
}
The push payload must contain property content-available and some custom data of your, but not title, subtitle or body fields.
{
"aps": {
"content-available" : 1,
},
"my_id": "001"
}
There are also some mandatory headers for POST request to APNs must be included:
- apns-push-type must be set to background
- apns-priority must be set to 5. Notification might not be delivered with other values.
Modifying push content
Sometimes you might want to update push payload to:
- modify data with some logic on device (i.e. decrypt content)
- download images or media whose size exceed maximum payload size
Modifying a remote notification requires special extension. From Xcode menu (File → New → Target… ) add Notification Service Extension.
The extension is quite simple, only two methods to implement
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var bestAttemptContent: UNMutableNotificationContent?
override func didReceive(
_ request: UNNotificationRequest,
withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void
) {
self.contentHandler = contentHandler
bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let bestAttemptContent = bestAttemptContent {
// Modify the notification content here...
bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"
contentHandler(bestAttemptContent)
}
}
override func serviceExtensionTimeWillExpire() {
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content,
// otherwise the original push payload will be used.
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
contentHandler(bestAttemptContent)
}
}
}
In the didReceive(_ request: withContentHandler)
method you create a mutable copy of notification content and change the parts you need. You have only 30 sec to perform it. After it, just before the extension is terminated, the second method serviceExtensionTimeWillExpire
will be called. Use it to deliver you best attempt to modify initial payload.
There are few important moments to mention:
- This extension only modify text for banner. If notification does not include banner or it’s prohibited by user, extension won’t be involved.
- If your app is in foreground, and you implemented
userNotificationCenter(_ center:willPresent:withCompletionHandler:)
method, you receive unmodified payload ofaps
. But modified fields ofUNNotificationContent
- Method
userNotificationCenter(_ center: UNUserNotificationCenter, didReceive)
still receive unmodified payload ofaps
when user tap on modified notification banner. Properties ofUNNotificationContent
will be updated, but not JSON itself. Donno if it a bug or feature. Ping me in comments your experience. - There are no modern async/await version of these functions :(
Custom Appearance of Notifications
Here is quote from documentation
When an iOS device receives a notification containing an alert, the system displays the contents of the alert in two stages. Initially, it displays an abbreviated banner with the title, subtitle, and two to four lines of body text from the notification. If the user presses the abbreviated banner, iOS displays the full notification interface, including any notification-related actions. The system provides the interface for the abbreviated banner, but you can customize the full interface using a notification content app extension.
Have you seen those pushes change their visual representation on force touch? Let’s make one. From Xcode menu (File → New → Target… ) add Notification Content Extension.
File “coin.png” added by me. Will present it later on UI.
Here are some configuration in Info.plist available.
- UNNotificationExtensionCategory may be a single string or an array of category names of push notification to apply this extension to. Since you may register several extension for different push notification categories, this is how you match them.
- UNNotificationExtensionDefaultContentHidden define if default UI (title and subtitle) should be visible together with your custom UI (see picture below).
- UNNotificationExtensionInitialContentSizeRatio defining aspect ratio of UI while loading in compare to width (see picture below).
- UNNotificationExtensionUserInteractionEnabled control if custom interaction elements available. i.e. you may add button and change UI on press. If this setting is NO, pressing on notification view will launch app, it YES — button will be pressed. Pressing on non-active elements will do nothing (no app launch!!!).
Some notes to remember:
- This extension must contain one controller only. I wonder why in Xcode 15.3 in a year 2024 they still use storyboards, but that’s how it is. Have you succeeded to use SwiftUI? Let me know how. I’ve seen people building UIKit UI programmatically. Nah…
- Loading time is quite long, I have no idea what system is doing. If you know, or know how to improve it, please let me know in comments.
Feel free to play with source code.