Tag Archives: UserNotification

Customizing Notifications with the Service and Content Extensions

For all the changes in notifications starting in iOS 10 the coolest is the service extension and the content extension. The service extension intercepts push payloads from apps, and gives you the chance to change content in the notification before it is presented. The Content extension gives you the same tools you have in an app to design your notification in interface builder.
Extensions are background applications, running independently from your app. They are around even when the app is not in the foreground. These two extensions offer some powerful new tools for notification development.

I’ll be continuing lessons from the last two posts. Since the first one requires registration with apple, there is no download. Before you do this, check out my  first post on making push notifications here and the second on push notification actions here. There’s lot of steps I’ll assume you already know in this lesson from those lessons.

User Info

So far, you’ve added everything to the aps dictionary in the payload. APNs does not restrict you to these few content objects.
Go to the payload editor. So far we have this:

{
    "aps":{
        "alert":{
            "title":"Push Pizza Co.",
            "body":"Your pizza is  almost ready!"
        },
        "badge":42,
        "sound":"default",
        "category":"pizza.category",
    }
}

I placed everything in the aps dictionary. The aps dictionary of the payload are system defined key value pairs. If you place things outside this dictionary, but in the JSON dictionary, they are user defined values. For example, place a comma after the aps dictionary ends and then add these two lines:

  "order":["Huli pizza","Lilikoi punch","Duke Pie"],
         "subtitle":"Your order is ready!"

On the first line I added a list for my order in an array. There’s no subtitles in the aps dictionary, yet  subtitle is a property in the notification content, so I put it as userInfo for now.

These store in the userInfo dictionary of the notification content. You have to move them from  userInfo to the right place in the notification.

The Service extension

Storing that data in the userInfo property of UNNotificationContent object might be great, but how to we use them in a push notification? In the UserNotifications framework, Apple introduced the Notification Service Extension. It’s a small program running in the background of your device that intercepts your payload before notification center presents your push notification. In the service extension, you can modify your content with a UNMutablenotificationContent object modifying your content. That can be decrypting a encoded message or grabbing data in the userInfo to modify the content.

To set up the Notification Service extension, you’ll need a new target in your application. In Xcode, go to File>New Target.

2017-01-22_08-22-35

Select iOS, and you’ll find in the list the notification service extension.

2017-01-22_08-23-05

Select it, click Next and it will ask for a product name. I usually name this by suffixing ServiceExtension to my app name, so make it PushPizzaDemoServiceExtention. Click Finish.

2017-01-22_08-24-16

You’ll get a question about activating the extension. Go ahead and activate it.

2017-01-22_08-24-29

You’ll find in the navigator a new group. This is the service extension. Open it up and you’ll find two new files.

2017-01-24_07-16-24

Open it up and you’ll find two new files. Click on the NotificationService.swift file. This is only two functions and two properties. The core method is the didRecieve(Request: withContentHandler:) . Apple even started coding for you. The method assigns the content handler closure to one of the two properties. The other property, bestAttemptContent is a mutable copy of your push notification’s content. In this method, you change the content for the push notification, loading all that rich content you couldn’t fit in the payload, or do any change to the content you wish. For example, Apple’s template changes the title.

if let bestAttemptContent = bestAttemptContent {
   // Modify the notification content here...
   bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"
            
   contentHandler(bestAttemptContent)
}

Always leave the function call to contentHandler there. That changes the content.

Go over to the payload. To use a notification extension, you must have the following key value pair in your payload’s aps dictionary:

"mutable-content":1 

Go over to your payload and add that to the payload, which should now look like this:

{
    "aps":{
        "alert":{
            "title":"Push Pizza Co.",
            "body":"Your pizza is  almost ready!"
        },
        "badge":42,
        "sound":"default",
        "category":"pizza.category",
        "mutable-content":1
    },
    "order":["Huli pizza", "Lilikoi punch","Duke pie"],
    "subtitle":"Your Order is ready",
    }

Coding the Service Extension with UserInfo

In the service extension, delete this line:

 bestAttemptContent.title = "\(bestAttemptContent.title) [modified]"

We’ll replace it with our code. I’m going to place the custom information into the body, giving a list of the order, and add the subtitle to the subtitle property. You’ll need the userInfo dictionary. I’ll make referring to the dictionary easier by making it a constant in the function, casting it to the JSON dictionary type of [String:Any]

//get the dictionary
let userInfo = bestAttemptContent.userInfo as! [String:Any]

Subtitles are user info in a payload but are a property of UNMutableNotificationContent, so it needs transferring to the property. Since it’s a dictionary entry, I’ll have to unwrap it and check for nil. If not nil, I have a subtitle and I can update the value. I downcast the value to String when assigning it the subtitle content, since the dictionary has the value as Any.

//change the subtitle
        if userInfo["subtitle"] != nil{
            bestAttemptContent.subtitle = userInfo["subtitle"] as! String
        }

The payload has an array of items ordered. I’ll make that the notification body text like this:

//change the content to the order
if let orderEntry = userInfo["order"] {
    let orders = orderEntry as! [String]
    var body = ""
    for item in orders{
        body += item + "\n "

    }
    bestAttemptContent.body = body
}

I’ll first check if the entry exists in the userInfo dictionary. If it does, create a string array orders from it. In the loop I’ll concatenate the orders into a string body, separating them with tabs. After the loop, I’ll change the bestAttemptContent.body to the new content.

The Content Extension

You’ve loaded content through the service extension. There’s another extension that gives you a storyboard to format your output, and add other types of objects. The Content extension gives you a view controller and storyboard to create your own notification look. TO add a content extension, go to File> New > Target. Under iOS you’ll find the service extension.

2017-01-24_07-22-37

Select it and click Next. Save a name of PushPizzaContentExtension. Click Finish, then activate.
You’ll find a new target on the navigator. Open it up and it is three files: A navigating controller, a storyboard and a info.plist.

2017-01-24_07-24-21

Open up the storyboard an you’ll find a very short looking scene. It’s here you will design your application.

2017-01-24_07-25-06

There’s one limitation to this storyboard. Interactive controls like buttons don’t work. You are always limited to static controls. With that in mind it’s time to design a custom notification

Change the UI

You’ll see the storyboard is a bit small, meant to cover only a small notification of the single label. Click the view controller icon and then go to the size inspector. The simulated size under freeform is 320 by 37.

2017-01-24_07-27-01

 

Change the Height to 150 to give us some space to work. This notification will look different on different devices, so use auto layout to place three labels. If you are not familiar with using auto layout, don’t fret. I’ll take you through the steps.

Delete the current label. Drag three new labels to the storyboard. Put two towards the top right and one towards the bottom left.

2017-01-23_05-58-53

Change the text of the top label to <Title>. I use angle brackets to document this is where I’m adding the content from the content.The middle one to <Subtitle> and the bottom to <Body>
On the <Title> label make the font Title1, right justified. It won’t fit, so drag to the left handles over so it goes completely across the scene, then right justify it. On the <Subtitle> label, set the text to Title 2, also right justifying. On the <Body> label, set the lines to 0 and the line break to word wrap. This will word wrap the order in the body if necessary.

2017-01-23_06-09-21

You’ll add some auto layout to this next. Click the <Title> label. Using the pin pinMenuButton tool, Pin up 10,left 4 and right 4 with the margins on, updating items of new constraints. Remember to press tab after every field.  The pin tool should look like this. Add the 3 constraints.

2017-01-23_06-11-40

The subtitle is a bit tricky. Pins in this storyboard always go to the superview. Using  the pin tool pinMenuButton  like you did for the title, pin the left and right sides of  subtitle  4 left and 4 right with constrain to margins on.

2017-01-24_07-30-58

Don’t update the view yet, just add the two constraints. Next control-drag from the subtitle to the title. Select vertical spacing in the menu that appears.

2017-01-24_07-34-43

In the size inspector find the constraint:

2017-01-24_07-35-43

Click Edit and change the constant to 10.

2017-01-24_07-37-12

Now click the update frames button update frames to update the frames.

Finally, select the body. Using the pin tool, pin it left 4, right,4 and bottom 10, with constrain to margins on and updating the frames. The completed layout should look like this:

2017-01-23_06-26-18

You have a layout for your notification. Just like an app, I’ll set my outlets.  Since I deleted a control,  Use Shift-Command-K to clean the project before going on. Close the attributes inspector. Open the assistant editor.

Control Drag from <Title>and make an outlet titleLabel.

Control-drag from <Subtitle>and make an outlet subtitleLabel.

Control-drag from <Body> and make an outlet bodylabel.

Close the assistant editor. I’ve now connected the the four outlets for the objects on the storyboard. The next step is to write some code.

Code the controller

Go to the NotificationViewController.swift file. Unlike most view controllers, there’s a special method here: didReceive: notification:. Besides a viewdidLoad, there’s no other methods you’re used to in a view controller. You’ll see in the method we have a template  on how to use it.

In this method’s parameter, we have the full notification. It could be either a push or a local notification. We extract the contents in this method and then populate the fields. Delete this code in the method.

self.label?.text = notification.request.content.body

To make things easier make a constant for the content of the notification:

let content = notification.request.content

I’ll use instead of notification.request.content every time. I can now populate the labels with their content.

titlelabel.text = content.title
subtitleLabel.text = content.subtitle
bodyLabel.text = content.body

With these three, we get all of our text labels into the notification.

The Property List

We’ve got one more step to go. Go back to Xcode. There one more file in the extension we’ve yet to look at. Click on the info.plist. Towards the bottom open the NSExtension. Inside that there’s several properties. Open the NSExtensionAttributes. You’ll find in there the UNNotificationExtensionCategory property.
Content extensions runs off categories, so whatever category this property is the category of the notification found in content.category. Change it to pizza.category. This extension will run for only the pizza.category notifications. If an extension has another category, it will not run. Remember to do this. If you forget this, the extension it will not work for you.

Run the Notification

You are ready to run the notification. When you add extensions, Xcode often defaults to the run scheme for the extension. Check to make sure you are on your device run scheme for the app

2017-01-23_06-41-43.

Run the application. Copy and paste the payload to the test platform. If you haven’t done so for this device, copy and paste the token for the app.

2017-01-24_06-37-53

Send the notification. When it appears like this
img_0036

Swipe down

img_0037

You get a custom notification, with alignment and font changes, but it is too big.

 

Resizing the Notification.

Go back to Xcode. The easiest way to restrict the notification is to add these two lines of code to viewDidLoad in the NotificationViewController.swift file in the content extension:

let size = view.bounds.size
preferredContentSize = CGSize(width: size.width, height: size.height / 4.0)

This code changes the height of the notification to a quarter the size of the notification. Depending on your notification, you can change this size by changing the 4.0. Run with this code, then send a notification from the test platform. When the notification appears on the device, swipe down. You get a better sized notification.
img_0038

Removing the Bottom Notification

You see in white on the bottom of the notification the original notification. You may not want that on your notification. Let me show you how to hide that. In Xcode, Go back to the info.plist  for the content extension again. Click the + for the NSExtentionAttributes.

2017-01-24_06-58-24

In the new attribute, give it a key UNNotificationExtensionDefaultContentHidden. Change it to a Boolean value, and set it to YES.

2017-01-24_06-58-52

Run again so it loads everything. Send the notification in the platform.The bottom disappears.
img_0039

If all you add to the storyboard is  new user content like  map, You may leave the white content to have the original message.

This is some of the basics of the service and content extensions. With a bit of work you can add images, maps and other objects to the extensions to make for a very rich notification for your users. Remember while the service extension is for push notifications only, you can use the content extension for local notifications and push notifications.

The Whole Code

payload.apns

{
    "aps":{
        "alert":{
            "title":"Push Pizza Co.",
            "body":"Your pizza is  almost ready!"
        },
        "badge":42,
        "sound":"default",
        "category":"pizza.category",
        "mutable-content":1
    },
    "order":["Huli pizza", "Lilikoi punch","Duke pie"],
    "subtitle":"Your Order is ready"
}

AppDelegate.Swift

//
//  AppDelegate.swift
//  PushPizzaDemo
//
//  Created by Steven Lipton on 1/2/17.
//  Copyright © 2017 Steven Lipton. All rights reserved.
//

import UIKit
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {

    var window: UIWindow?

    func setCategories(){
        let snoozeAction = UNNotificationAction(identifier: "snooze.action", title: "Snooze", options: [])
        let pizzaCategory = UNNotificationCategory(identifier: "pizza.category", actions: [snoozeAction], intentIdentifiers: [], options: [])
        UNUserNotificationCenter.current().setNotificationCategories([pizzaCategory])
    }
    
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        setCategories()
        UNUserNotificationCenter.current().delegate = self
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert,.sound,.badge])
        {(granted,error) in
            if granted{
                application.registerForRemoteNotifications()
            } else {
                print("User Notification permission denied: \(error?.localizedDescription)")
            }
            
        }
        return true
    }
    
    func tokenString(_ deviceToken:Data) -> String{
        //code to make a token string
        let bytes = [UInt8](deviceToken)
        var token = ""
        for byte in bytes{
            token += String(format: "%02x",byte)
        }
        return token
    }
    
    func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
        //TODO: Add code to get token here
        print("Successful registration. Token is:")
        print(tokenString(deviceToken))
    }
    
    func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
        print("Failed to register for remote notifications: \(error.localizedDescription)")
    }



//MARK: - Delegates
    
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        let request = notification.request
        print ("request identifier: \(request.identifier)" )
        completionHandler([.alert,.badge,.sound])
    }
    
    
    func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        let action = response.actionIdentifier
        let request = response.notification.request
        
        if action == "snooze.action"{
            let content = changePizzaNotification(content: request.content)
            
            let snoozeTrigger = UNTimeIntervalNotificationTrigger(timeInterval: 5.0, repeats: false)
            let snoozeRequest = UNNotificationRequest(identifier: "pizza.snooze", content: changePizzaNotification(content:content), trigger: snoozeTrigger)
            center.add(snoozeRequest){
                (error) in
                if error != nil {
                    print("Snooze Request Error: \(error?.localizedDescription)")
                }
            }
        }
        completionHandler()
    }
    
    func changePizzaNotification(content oldContent:UNNotificationContent) -> UNMutableNotificationContent{
        //get a mutable copy of the content
        let content = oldContent.mutableCopy() as! UNMutableNotificationContent
        //get the dictionary
        let userInfo = content.userInfo as! [String:Any]
        //change the subtitle
        if userInfo["subtitle"] != nil{
            content.subtitle = userInfo["subtitle"] as! String
        }
        //change the body with the order
        if let orderEntry = userInfo["order"] {
            var body = ""
            let orders = orderEntry as! [String]
            for item in orders{
                body += item + "🍕🏄🏽\n"
            }
            content.body = body
        }
        return(content)
    }
}

PushPizzaDemoServiceExtension: NotificationService.swift

//
//  NotificationService.swift
//  PushPizzaDemoServiceExtension
//
//  Created by Steven Lipton on 1/5/17.
//  Copyright © 2017 Steven Lipton. All rights reserved.
//

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)
        
        if let bestAttemptContent = bestAttemptContent {
            let userInfo = bestAttemptContent.userInfo as! [String:Any]
            //change the subtitle
            if userInfo["subtitle"] != nil{
                bestAttemptContent.subtitle = userInfo["subtitle"] as! String
            }
            //change the content to the order
            if let orderEntry = userInfo["order"] {
                let orders = orderEntry as! [String]
                var body = ""
                for item in orders{
                    body += item + "\n "
                    
                }
                bestAttemptContent.body = body
            }
            
            
            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)
        }
    }
 
    func changePizzaNotification(content oldContent:UNNotificationContent) -> UNMutableNotificationContent{
        let content = oldContent.mutableCopy() as! UNMutableNotificationContent
        //get the dictionary
        let userInfo = content.userInfo as! [String:Any]
        //change the subtitle
        if let subtitle = userInfo["subtitle"]{
            content.subtitle = subtitle as! String
        }
        
        //change the body with the order
        if let orderEntry = userInfo["order"] {
            var body = ""
            let orders = orderEntry as! [String]
            for item in orders{
                body += item + ", "
            }
            content.body = body
        }
        return content
    }
}

PushPizzaDemoServiceExtension: NotificationViewController.swift


//
//  NotificationViewController.swift
//  PushPizzaDemoContentExtension
//
//  Created by Steven Lipton on 1/5/17.
//  Copyright © 2017 Steven Lipton. All rights reserved.
//

import UIKit
import UserNotifications
import UserNotificationsUI

class NotificationViewController: UIViewController, UNNotificationContentExtension {

    @IBOutlet weak var titlelabel: UILabel!
    @IBOutlet var subtitleLabel: UILabel!
    @IBOutlet var bodyLabel: UILabel!
    override func viewDidLoad() {
        super.viewDidLoad()
        
//set the proportional vertical size of the notification.
        let size = view.bounds.size
        preferredContentSize = CGSize(width: size.width, height: size.height / 4.0)
    }
    
    func didReceive(_ notification: UNNotification) {
        let content = notification.request.content
        titlelabel.text = content.title
        subtitleLabel.text = content.subtitle
        bodyLabel.text = content.body
    }
}

Add Actions and Categories to Notification in Swift

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:

2016-12-01_06-17-19

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.
2016-12-02_06-21-08
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.

2016-12-02_07-38-33

Press the Start button. Wait ten seconds. The notification appears.

2016-12-02_07-39-19

 

You can swipe down from the notification and see the action, but it does nothing. There’s no code for it do anything.

2016-12-02_07-40-31

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.

2016-12-02_07-50-03

 

Since we copied the content, you can swipe down again, and snooze as many times as you like.

2016-12-02_07-52-22

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.

2016-12-02_07-58-22

Swipe to the left, and two buttons appear.

2016-12-02_07-58-40

Tap View. the actions appear.

2016-12-02_07-58-59

Tap Add Comment. The keyboard should appear in the simulator. If it does not press Command-K.

2016-12-02_07-59-20

Type a comment then tap Add.

2016-12-02_08-00-25

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.

2016-12-02_08-00-56

Tap the notification, open the phone with command-shift-H and app title has changed.

2016-12-02_08-01-18

 

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.

img_7915

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.

img_7913

Tapping a text action will run the code on your phone for the notification. You’ll see the notification on your watch

img_7917

And the text label on the phone changes.

img_7918

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()
    }
}