Rich notifications with Vapor - Part 2: iOS Application
You will need Xcode 10+, Vapor 3+, Cocoapods, and some understanding of Vapor.
Introduction
In this part of the tutorial we will be building an iOS application that handles receiving a push notification with rich content. The application will only show a notification in the notification center. We won’t be handling the notification when we have the application open.
Sometimes sending notifications with only text just isn’t quite enough. This is where rich notifications come in. We can add images and videos to our notification to give the user a richer experience directly from there notification center.
Prerequisites
- Make sure you have completed part one of this tutorial.
- A basic understanding of Vapor - please complete my “Getting started with Vapor” Part One and Part Two.
- Xcode 10+
- MacOS
- Vapor 3.0 - Install instructions here.
- An iOS device for testing notifications.
- An understanding of iOS development and Xcode environment.
- Cocoapods - Install instructions here.
- A REST client such as Postman and a basic understanding of how to use it.
Handling basic notifications
Adding our dependency
Start by creating a new single view application and name it whatever you wish. Open the terminal and go to the working directory of the newly created project and run the following command.
$ pod init
Open the newly created Podfile
and add the following pod:
pod 'PushNotifications'
In the terminal run:
$ pod install
Make sure you close your Xcode project and reopen the newly created Xcode Workspace (YOUR-APP.xcworkspace) before continuing.
Set up notifications
Within your project capabilities make sure you have switched on the Push Notifications capability. Also turn on the Background Modes capability and tick the box for Remote Notifications.
Open your AppDelegate.swift
file and replace its contents with the following. Remembering to replace the instance ID with your own.
// AppDelegate.swift
import UIKit
import PushNotifications
@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
let pushNotifications = PushNotifications.shared
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
self.pushNotifications.start(instanceId: "YOUR_INSTANCE_ID")
self.pushNotifications.registerForRemoteNotifications()
try? self.pushNotifications.subscribe(interest: "general")
return true
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
self.pushNotifications.registerDeviceToken(deviceToken)
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
self.pushNotifications.handleNotification(userInfo: userInfo)
}
}
You can now run the application on your iOS device and accept to receive notifications. The SDK will then manage registering our interest in “general”. This is the same interest that we have defined in our server that we are pushing to.
If we were to send a notification now we would see a notification on the lock, however it would not show any rich content.
Handling rich notifications
Create a new target
Rich notifications are handled by a separate target within our application. To add our new target go to File → New → Target. Select the Notification Service Extension.
Note: If you wish to customize how a notification will look on the lock screen. You can use the Notification Content Extension to do this. You may find this tutorial useful if you wish to learn more about this.
On the next screen give your target a sensible name and make sure it is set to the correct project and embed in the correct application. Once you have confirmed this click Finish. You might be asked to activate your target, select Activate.
Handle download
Your new target will create a new NotificationService.swift
file. This file will handle the notification you have received and attempt to show it.
// NotificationService.swift
import UserNotifications
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)
// Get the custom data from the notification payload
if let notificationData = request.content.userInfo["data"] as? [String: Any] {
// Grab the attachment
if let urlString = notificationData["attachment-url"] as? String,
let fileUrl = URL(string: urlString) {
// Download the attachment
URLSession.shared.downloadTask(with: fileUrl) { (location, response, error) in
if let location = location {
// Move temporary file to remove .tmp extension
let tmpDirectory = NSTemporaryDirectory()
let tmpFile = "file://".appending(tmpDirectory).appending(fileUrl.lastPathComponent)
let tmpUrl = URL(string: tmpFile)!
try! FileManager.default.moveItem(at: location, to: tmpUrl)
// Add the attachment to the notification content
if let attachment = try? UNNotificationAttachment(identifier: "", url: tmpUrl) {
self.bestAttemptContent?.attachments = [attachment]
}
}
// Serve the notification content
self.contentHandler!(self.bestAttemptContent!)
}.resume()
}
}
}
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)
}
}
}
It may feel like a lot is going on here but in fact it is all quite simple. We simply check to see if we have a URL within our payload. We then use this URL to download the content to a temporary file and attempt to display this.
Note: Rich notifications have a timeout called by the system, if the original content has not been downloaded then the original payload without the rich content will be displayed. It is therefore considered best practice to make sure the notification makes sense without the rich content.
Testing your integration
Now we can begin testing our integration. If you have closed your Vapor project reopen it in the usual way in Xcode and hit the run button. You should see a message in the console saying it has started and is listening on http://localhost:8080. To begin our first make sure the iPhone is on the lock screen. Our test requires us to use Postman (or another REST client) to pass data through. Open postman or your equivalent REST client and set it up so that it looks like this.
This will show a notification like below. You can force touch to see the full image like the image on the right.
You can change the URL in the post request to contain anyone of the types discussed in the first part of this tutorial. For example you may which to show a video, gif or audio clip.
Conclusion
In this part of the tutorial we have learnt how to handle both text notifications and rich notifications in our iOS application. We also learnt how we can test the notifications using postman and change the data that we are sending.
The source code for this project can be found here.
18 March 2019
by Christopher Batin