In earlier lessons I’ve shown you how to make a notification and how to manage notifications with the UserNotifications
frame work. One very exciting part of the frame work is executing the app’s code in the background. You don’t have to open the app to do custom actions or even to input data. Using categories and actions you can build code that does that directly from the notification. In this lesson I’ll show you how to use this powerful feature.
Categories, Actions and Delegates — Oh my!
Notifications have two special objects called categories and actions. Actions are controls added to a notification, usually a button. Categories are a set of actions we can link to the content of a notification. You can mix and match categories and actions as much as you wish.
Actions have no executable code. Instead you specify in a method of UNUserNotificationCenterDelegate
the code based on an identifier in the action.
This makes for a very flexible system for adding small bits of code to a application. There are time and memory limits on the code, so keep it short and simple.
Setting up the Demo
Let’s set up an example application to show a very simple alarm application. The alarm will time for 10 seconds, then display an alarm and sound a sound. We’ll add two actions to this alarm: one as a five second snooze button and the other to place a comment on the notification. You’ll find a starter file here notificationcategorydemo_start if you want to skip this section.
Open a new single view project in Xcode called NotificationCategoryDemo. Make it a Swift Application with a Universal device.
Go to the storyboard. Add a label and a button. I’m not going to get fancy here, but I set up my button and label like this:
I used the Title 1 font. I suggest changing the attributes of the label. Set the Lines to 0 and Line Break to Word Wrap. This way, long text entries will word wrap by themselves.
Open to the assistant editor and control-drag from the button to the code to make an IBAction
named StartButton. Control drag from the label to the code and make a IBOutlet
named commentsLabel. Close the assistant editor and go to Viewcontroller.swift.
I’m going to go fast here and just give you the code. I’ll assume you’ve read the post on how to make a user notification. Under import UIKit
, add the following:
import UserNotifications
Add the following properties and constants to the ViewController
class
let time:TimeInterval = 10.0 let snooze:TimeInterval = 5.0 var isGrantedAccess = false
Set up the required authorization check. Change viewDidLoad
to this:
override func viewDidLoad() { super.viewDidLoad() UNUserNotificationCenter.current().requestAuthorization( options: [.alert,.sound,.badge], completionHandler: { (granted,error) in self.isGrantedAccess = granted if granted{ self.setCategories() } else { let alert = UIAlertController(title: "Notification Access", message: "In order to use this application, turn on notification permissions.", preferredStyle: .alert) let alertAction = UIAlertAction(title: "Okay", style: .default, handler: nil) alert.addAction(alertAction) self.present(alert , animated: true, completion: nil) } })
Create a function setCategories
which we’ll use in the lesson and resolve the error in viewDidLoad
func setCategories(){ }
Create a function addNotification
which simplifies adding notifications to the rest of the code. I particularly hate retyping that error handler closure a zillion times.
func addNotification( content:UNNotificationContent, trigger:UNNotificationTrigger?, indentifier:String) { let request = UNNotificationRequest( identifier: indentifier, content: content, trigger: trigger) UNUserNotificationCenter.current().add(request, withCompletionHandler: { (errorObject) in if let error = errorObject{ print("Error \(error.localizedDescription) in notification \(indentifier)") } } ) }
We’ll add the notification from the Start button. Change the startButton
method to this:
@IBAction func startButton(_ sender: UIButton) { if isGrantedAccess{ let content = UNMutableNotificationContent() content.title = "Alarm" content.subtitle = "First Alarm" content.body = "First Alarm" content.sound = UNNotificationSound.default() let trigger = UNTimeIntervalNotificationTrigger( timeInterval: time, repeats: false) addNotification( content: content, trigger: trigger , indentifier: "Alarm") } }
I did add one new type of content I haven’t talked about before. If you specify a value in the sound
property of your notification content and grant permission for using a sound, sounds will play during your notification. I’m using the default sound available at UNNotificationSound.default()
.
I’ll use in-app notification in this application, and you’ll need the delegate anyway for actions. Add the UNUserNotificationDelegate
to the ViewController
class:
class ViewController: UIViewController, UNUserNotificationCenterDelegate {
Set the delegate
to self
in viewDidLoad
UNUserNotificationCenter.current().delegate = self
Then add to your code the userNotificationCenter(willpresent notification:...)
method.
// MARK: - Delegates func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.alert,.sound]) }
Be certain to add .sound
to hear the sound in the in-app notification.
The setup is complete. You can download a starter file by clicking notificationcategorydemo_start
Adding Categories and Actions
Categories and actions must register with the system before you make any notifications. Generally you do that in viewDidLoad
. If you look at the code there, you’ll find I set a function call to setCategories
if notification access is granted. You’ll add the categories and actions in setCategories
.
Actions are contained in categories. Add actions first then categories. Actions are objects of class UNNotificationAction
. You use a constructor for UNNotificationAction
to make an actions. Add this to the setCategories
function.
func setCategories(){ let snoozeAction = UNNotificationAction( identifier: "snooze", title: "Snooze 5 Sec", options: [])
There’s three parameters here. The identifier
is a unique string to identify the action in the delegate. The title is the button’s title when the actions display on the device. The options
parameter are a list of options you can change the behavior or look of the button, such as a destructive button or forcing authentication before carrying out an action. Leave this blank to keep things simple.
Add this action to a category which we’ll call alarmCategory
. Categories are UNNotificationCategory
objects, and like UNNotificationAction
, have a constructor. Add this to your code:
let alarmCategory = UNNotificationCategory( identifier: "alarm.category", actions: [snoozeAction], intentIdentifiers: [], options: [])
There are four parameters for this constructor. There is a unique identifier
which you’ll use to link the category to a notification’s content. The actions
parameter is an array of actions associated with in this category. You can have several categories which mix and match actions differently for different notifications or different contexts for a single notification. The parameter intentIdentifiers
is a Siri thing and that’s way beyond the scope of this lesson, so leave it blank. Finally, there are options
for a custom dismiss action and allowing CarPlay to use the actions. Again, I left the options
blank.
The final step is to register the categories in the current UserNotificationCenter
. You specify a set of categories the system should know. Add this code.
UNUserNotificationCenter.current().setNotificationCategories([alarmCategory])
Since we have only one category, this is a very simple list.
Using Categories in Notifications
Having your categories set up, add them to your notification content. The UNMutableNotificationContent
has a property categoryIdentifier
. In the startButton
method, specify the category identifier in your content.
content.categoryIdentifier = "alarm.category"
You are almost set to run your code. Set your simulator to iPhone 6. There’s one feature of the simulator that rides close to that thin line of being a feature or a bug. On anyone not using a 3D touch trackpad it is a bug from my perspective. The simulator assumes you have 3D Touch on your Mac if you simulate devices that have 3D touch. Accessing notification actions requires 3D touch on those devices in the simulator. Setting your simulator to an iPhone 6 lets any Mac running the simulator access the actions.
Build and run. The app appears.
Press the Start button. Wait ten seconds. The notification appears.
You can swipe down from the notification and see the action, but it does nothing. There’s no code for it do anything.
Make an Action Do Something
It is the user notification center’s delegate that does the heavy lifting. The userNotficationCenter(didReceive response: completionHandler:)
method is based on the actionIdentifier
you defined earlier. The delegate method executes a bit of code to handle that action.
Where you keep your delegates in your code, add this.
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { }
Before you do anything else, add the completionHandler
to the bottom of the code. In this method, the system uses the closure, but at the end of your code you have to call it. I try to add it first so I don’t forget it.
completionHandler()
The UNNotificationResponse
object has two properties: the actionIdentifier
of the action that fired, and the delivered notification
. I tend to be most interested in the request, so make two constants to work with these values easily.
let identifier = response.actionIdentifier let request = response.notification.request
The identifier
is a string I can compare to the identifier names I defined in the setCategories
method. So to execute actions for the snooze action identifier, add an if
clause
if identifier == "snooze"{ }
Inside the if
clause, add code to do the actions when the user presses the Snooze 5 Sec button. For this demo, I’ll make a notification that fires five seconds later. You need mutable content from the request. Add this inside the if
clause:
let newContent = request.content.mutableCopy() as! UNMutableNotificationContent
By copying the old request’s content, all content is set up. You just have to change the subtitle and body like this:
newContent.body = "Snooze 5 Seconds" newContent.subtitle = "Snooze 5 Seconds"
The trigger will change from 10 seconds to 5 seconds. Define a new trigger method.
let newTrigger = UNTimeIntervalNotificationTrigger(timeInterval: snooze, repeats: false)
Send the new trigger and content. Add a notification using the addNotification
method you defined earlier.
addNotification(content: newContent, trigger: newTrigger, indentifier: request.identifier)
Build and run. Tap the start button and wait. When the notification appears, swipe down to see the button. Tap the snooze button, and five seconds later, the snooze notification appears.
Since we copied the content, you can swipe down again, and snooze as many times as you like.
Add Text Input
There’s a subclass of UNNotificationAction
you’ve used if you replied to a text message in a notification. There is a text input action in notifications. Adding text input, with a few variations, is the same as adding any other action.
There’s two constructors for the UNTextinputNotificationAction
object. The shorter has the same parameters as the UNNotificationAction
. The longer of the two adds two parameters for placeholder text and the submit button title. Use that one for this project, adding this to setCategories
, between the snoozeAction
and the alarmCategory
.
let commentAction = UNTextInputNotificationAction(identifier: "comment", title: "Add Comment", options: [], textInputButtonTitle: "Add", textInputPlaceholder: "Add Comment Here")
Add the action to the category. Change the actions
parameter for the alarmCategory
from [snoozeAction]
to [snoozeAction,commentAction]
.
let alarmCategory = UNNotificationCategory(identifier: "alarm.category", actions: [snoozeAction,commentAction], intentIdentifiers: [], options: [])
In the delegate method usernotificationCenter( didReceive response: completionHandler:)
add another if
clause:
if identifier == "comment"{ }
The delegate method thinks that response is a UNNotificationResponse
, not a UNTextInputNotificationResponse
. In the if
clause, downcast the response to the proper type.
let textResponse = response as! UNTextInputNotificationResponse
There’s one extra property on a UNTextInputNotificationResponse
: a string named userText
. Send that string to the label in the app:
commentsLabel.text = textResponse.userText
Make new notification that will include your comment. Use the body in the notification for your comment:
let newContent = request.content.mutableCopy() as! UNMutableNotificationContent newContent.body = textResponse.userText addNotification(content: newContent, trigger: request.trigger, indentifier: request.identifier)
Build and run. Press Start, then Command-L to lock the screen. Ten seconds later the notification appears.
Swipe to the left, and two buttons appear.
Tap View. the actions appear.
Tap Add Comment. The keyboard should appear in the simulator. If it does not press Command-K.
Type a comment then tap Add.
Wait and the notification appears with your comment. Body text can be as long as you want, as long as it is one character. An empty body hides the notification.
Tap the notification, open the phone with command-shift-H and app title has changed.
Actions on Apple Watch
It’s not that difficult to add actions to a notification. When you have some code you want to execute without opening the app, it is extremely useful to use actions. As an added benefit, if you create an action on your iPhone app’s notification, any user with an Apple Watch will get both the notification and the actions on their watch. When the phone if sleeping or locked, the notification will go to the watch, with the actions below the notification.
The text action in Add Comment uses the text input system of the watch, so you can dictate, scribble, use emoji or your quick phrases.
Tapping a text action will run the code on your phone for the notification. You’ll see the notification on your watch
And the text label on the phone changes.
You never have to pull your phone out of your pocket or bag to respond with actions. With not that much code, you get a lot of performance with notification actions.
The Whole Code
There is a download of the completed project here: notificationcategorydemo.zip
// // ViewController.swift // NotificationCategoryDemo // // Created by Steven Lipton on 12/2/16. // Copyright © 2016 Steven Lipton. All rights reserved. // import UIKit import UserNotifications class ViewController: UIViewController,UNUserNotificationCenterDelegate { //MARK: Properties and outlets let time:TimeInterval = 10.0 let snooze:TimeInterval = 5.0 var isGrantedAccess = false @IBOutlet weak var commentsLabel: UILabel! //MARK: - Functions func setCategories(){ let snoozeAction = UNNotificationAction(identifier: "snooze", title: "Snooze 5 Sec", options: []) let commentAction = UNTextInputNotificationAction(identifier: "comment", title: "Add Comment", options: [], textInputButtonTitle: "Add", textInputPlaceholder: "Add Comment Here") let alarmCategory = UNNotificationCategory(identifier: "alarm.category",actions: [snoozeAction,commentAction],intentIdentifiers: [], options: []) UNUserNotificationCenter.current().setNotificationCategories([alarmCategory]) } func addNotification(content:UNNotificationContent,trigger:UNNotificationTrigger?, indentifier:String){ let request = UNNotificationRequest(identifier: indentifier, content: content, trigger: trigger) UNUserNotificationCenter.current().add(request, withCompletionHandler: { (errorObject) in if let error = errorObject{ print("Error \(error.localizedDescription) in notification \(indentifier)") } }) } //MARK: - Actions @IBAction func startButton(_ sender: UIButton) { if isGrantedAccess{ let content = UNMutableNotificationContent() content.title = "Alarm" content.subtitle = "First Alarm" content.body = "First Alarm" content.sound = UNNotificationSound.default() content.categoryIdentifier = "alarm.category" let trigger = UNTimeIntervalNotificationTrigger(timeInterval: time, repeats: false) addNotification(content: content, trigger: trigger , indentifier: "Alarm") } } override func viewDidLoad() { super.viewDidLoad() UNUserNotificationCenter.current().delegate = self UNUserNotificationCenter.current().requestAuthorization( options: [.alert,.sound,.badge], completionHandler: { (granted,error) in self.isGrantedAccess = granted if granted{ self.setCategories() } else { let alert = UIAlertController(title: "Notification Access", message: "In order to use this application, turn on notification permissions.", preferredStyle: .alert) let alertAction = UIAlertAction(title: "Okay", style: .default, handler: nil) alert.addAction(alertAction) self.present(alert , animated: true, completion: nil) } }) } // MARK: - Delegates func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.alert,.sound]) } func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { let identifier = response.actionIdentifier let request = response.notification.request if identifier == "snooze"{ let newContent = request.content.mutableCopy() as! UNMutableNotificationContent newContent.body = "Snooze 5 Seconds" newContent.subtitle = "Snooze 5 Seconds" let newTrigger = UNTimeIntervalNotificationTrigger(timeInterval: snooze, repeats: false) addNotification(content: newContent, trigger: newTrigger, indentifier: request.identifier) } if identifier == "comment"{ let textResponse = response as! UNTextInputNotificationResponse commentsLabel.text = textResponse.userText let newContent = request.content.mutableCopy() as! UNMutableNotificationContent newContent.body = textResponse.userText addNotification(content: newContent, trigger: request.trigger, indentifier: request.identifier) } completionHandler() } }
Leave a Reply