9.
Custom Actions
Written by Scott Grosch
At this point, you’ve implemented as much of push notifications as most app developers will ever want or need to do.
Don’t give up now! There are still some really amazing features you can add to your app to make it shine, should you so desire.
In the previous chapter, you built an app that triggers an action when the user taps on a received notification. Sometimes, a simple tap is not enough. Maybe your friend is asking you to grab coffee, and you want an easy way to accept the offer. Or maybe another friend posted a funny tweet and you want to favorite it right from the notification.
Thankfully, iOS gives you a way to attach buttons to a push notification so that the user can provide a meaningful response to the received notification without having to open your app! In this chapter, you’ll learn how to make your notifications actionable.
After opening up the starter project for this chapter, remember to turn on the Push Notifications capability as discussed in Chapter 4, “Xcode Project Setup,” and set the team signing as discussed in Chapter 7, “Expanding the Application.”
Categories
Notification categories allow you to specify up to four custom actions per category that will be displayed with your push notification. Keep in mind that the system will only display the first two actions if your notification appears in a banner, so you always want to configure the most relevant actions first.
To enable the user to decide what action to take, you’ll add Accept and Reject buttons to your push notifications.
You’ll first add an enum to the top of AppDelegate.swift, right below the import statements, to identify your buttons.
private let categoryIdentifier = "AcceptOrReject"
private enum ActionIdentifier: String {
case accept, reject
}
Use an enum to ensure you aren’t hardcoding strings for identifiers as you won’t ever display them to an end user.
Once that’s done, just add a method at the bottom of AppDelegate to perform the registration.
private func registerCustomActions() {
let accept = UNNotificationAction(
identifier: ActionIdentifier.accept.rawValue,
title: "Accept")
let reject = UNNotificationAction(
identifier: ActionIdentifier.reject.rawValue,
title: "Reject")
let category = UNNotificationCategory(
identifier: categoryIdentifier,
actions: [accept, reject],
intentIdentifiers: [])
UNUserNotificationCenter.current()
.setNotificationCategories([category])
}
Here, you create a notification category with two buttons. When a push notification arrives with a category set to AcceptOrReject, your custom actions will be triggered, and iOS will include the two buttons at the bottom of your push notification.
While I’ve simply hardcoded the titles here for brevity, you should always use a localized string, via the NSLocalizedString method.
Note: Even if you don’t think you’re going to localize your app, it’s better to get in the habit now than have to go back and find every single user visible string later if plans change!
You only need to register your actions if you’re actually accepting push notifications, so add a call to registerCustomActions() once you’ve successfully registered for remote notifications.
func application(
_ application: UIApplication,
didRegisterForRemoteNotificationsWithDeviceToken
deviceToken: Data) {
sendPushNotificationDetails(
to: "http://192.168.1.1:8080/api/token",
using: deviceToken
)
registerCustomActions()
}
Build and run your app. Now, go back into the push notification tester app (as described in Chapter 5, “Apple Push Notifications Servers”) and use the following payload:
{
"aps": {
"alert": {
"title": "Long-press this notification"
},
"category": "AcceptOrReject",
"sound": "default"
}
}
The critical part of the payload is making sure the category value exactly matches what you specified during your registration with UNUserNotificationCenter. Send another push to yourself now.
See your action buttons? No? Don’t worry, you didn’t mess anything up!
The trick is you need to long press the notification to reveal the buttons.
Once you do that, the custom buttons appear and you can select one.
Go back into your AppDelegate.swift and add the userNotificationCenter(_:didReceive:withCompletionHandler:) delegate method to your UNUserNotificationCenterDelegate extension:
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse,
withCompletionHandler completionHandler: @escaping () -> Void) {
defer { completionHandler() }
let identity = response.notification
.request.content.categoryIdentifier
guard identity == categoryIdentifier,
let action = ActionIdentifier(rawValue: response.actionIdentifier) else {
return
}
print("You pressed \(response.actionIdentifier)")
}
Remember that this method will be called when you tap on the notification, so you need to do a quick check to make sure you’re handling your own category, and then you can grab the button that was pressed. This method will be called even when your app is not in the foreground, so be careful of what you do here!
Build and run your code again, and send yourself another notification from the Tester app. Long press the notification and select one of the buttons; you should see a similar message in Xcode’s console:
You pressed accept
Extending Foundation’s notification
The AppDelegate is definitely not where you want to take action when a push notification arrives. A better idea is to send a Foundation notification that lets you know what happened, providing your other view controllers with the opportunity to take appropriate action. The SDK makes Foundation’s notifications a bit awkward to work with, but you’re going to fix that with a simple protocol extension.
Create a new Swift file called Notification.swift and replace its contents with the following:
import Foundation
extension Notification.Name {
// 1
static let acceptButton = Notification.Name("acceptTapped")
static let rejectButton = Notification.Name("rejectTapped")
// 2
func post(
center: NotificationCenter = NotificationCenter.default,
object: Any? = nil,
userInfo: [AnyHashable : Any]? = nil) {
center.post(name: self, object: object, userInfo: userInfo)
}
// 3
@discardableResult
func onPost(
center: NotificationCenter = NotificationCenter.default,
object: Any? = nil,
queue: OperationQueue? = nil,
using: @escaping (Notification) -> Void)
-> NSObjectProtocol {
return center.addObserver(
forName: self,
object: object,
queue: queue,
using: using)
}
}
There’s not a lot to the code, but it provides a ton of functionality!
- First, you’re defining two custom
Notification.Namestatic properties to represent each of your custom actions. You’ll post the appropriate one based on what happened. The string you use for the name doesn’t matter; it just has to be unique. - Any
Notification.Name, like the two that you just defined, now has apostmethod with the most commonly used defaults set for its arguments. - You’ve also provided a simple way to take an action when a post occurs. Again, defaults allow for the normal operation without boilerplate code.
Back in your AppDelegate’s userNotificationCenter(_:didReceive:withCompletionHandler:) you can now replace your print statement with the following:
switch action {
case .accept:
Notification.Name.acceptButton.post()
case .reject:
Notification.Name.rejectButton.post()
}
By adding that simple protocol extension to Notification.Name, you have made the rest of your code much easier to understand.
If your payload contains custom keys-values that you also need to pass along, you can simply change the post method to accept the data from the push notification, like so:
let userInfo = response.notification.request.content.userInfo
switch action {
case .accept:
Notification.Name.acceptButton.post(userInfo: userInfo)
case .reject:
Notification.Name.rejectButton.post(userInfo: userInfo)
}
Responding to the action
Now that you’ve properly detected that your end user has selected a custom action, and you’ve posted the notification, you need to actually do something. Return to ViewController.swift. You’ll see a little bit of existing code that simply keeps the label display value in sync with the counters.
Override viewDidLoad with the code below by simply adding it below your outlets. This code responds to the Foundation notifications you post when the user interacts with your notification, and updates the appropriate counter based on the user’s selection:
override func viewDidLoad() {
super.viewDidLoad()
Notification.Name.acceptButton.onPost { [weak self] _ in
self?.numAccepted += 1
}
Notification.Name.rejectButton.onPost { [weak self] _ in
self?.numRejected += 1
}
}
Creating your quick extension on Notification.Name has definitely paid off, hasn’t it? If the user presses the Accept button, you’ll increment the numAccepted counter; if they press the Reject button, you’ll increment the numRejected counter instead.
Build and run the app one final time. Send yourself a bunch of notifications, alternating between which button you tap each time. The display should keep a count for you!
By using Foundation’s notification service, you’ve kept the logic of your app encapsulated into the right areas, and you haven’t gone through convolutions to let the UNUserNotificationCenterDelegate methods know anything about the view controllers, which might or might not want to take action based on a notification.
Making notifications actionable is a good idea whenever it makes sense, as it streamlines the experience for the user and makes their life a little bit easier. Another added benefit is the fact these actions are also automatically displayed for users of the Apple Watch!
Key points
- You can make a push notification actionable by attaching a button to a notification.
- Notification categories allow you to specify up to four custom actions per category that will be displayed with your push notification.
- Since you do not want the AppDelegate to be where you take action when a push notification arrives, send a Foundation notification that reports what happened instead. That allows your other view controllers to take an appropriate action.