Tag Archives: actions

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

 

 

Adding Actions to iOS and WatchOS Local Notifications

Even if you never want to make an app for the Apple Watch, there’s one place you might want to think about supporting: Notifications. In a previous lesson, we explored how to make local notifications for iOS devices. That lesson ended with a free bonus: If a phone is sleeping and you have an Apple Watch, the notification will go to the watch.

Starting in iOS8, notifications get one big change: they can have actions of their own. On phone, tablet or watch this gives you more flexibility in how to respond to a notification. Suppose you had an app that after an appointment comes up, nags you about it every ten seconds until you stop the nagging. You could of course go into the app and shut it off, but it might be easier to have a stop nagging button on the notification. In this lesson, we’ll make that stop nagging button first for iOS devices, then for the Apple Watch. Along the way we’ll discover why watch notifications are the one place iOS developers should seriously think about for their phone apps.

Making a Demo Program – A Review of Notifications

In a previous lesson we went into detail about local notifications. We’ll be using a lot of what was learned there. In this tutorial, we’ll build a simple app to make a series of notifications similar to the HIIT app at the end of that lesson. We’ll use this as a review and quick introduction to local notifications if you are not familiar with them.

Since we will eventually get to some watch related notifications, make a new project using the iOS with Watch OS App template.

2016-04-26_15-32-23

Name the project PhoneWatchNotificationDemo and make sure watch notifications is checked on and everything else is off.

2016-04-26_15-32-22

Once you save the project, add a new file by pressing Command-N called NaggyNotification subclassing NSObject. Add these properties to the class

class NaggyNotification: NSObject {
    private let basicNags = ["Nag", "Nag nag","Naggity nag"]
    private let emojiNags = ["😀","😉","🙄","😒","😠","😡"]
    private var nags = ["Nag", "Nag nag","Naggity nag"]
    private var counter = 0
    private var timer = NSTimer()
    private var backgroundTask = 0
    private var localNotification = UILocalNotification()
    var nagging:Bool {return timer.valid } //read only
}

If you don’t know how to type emojis, press Control-Command-Spacebar to get the symbols palette.

We are going to do something insane a little later with this class. To protect against some of the problems with that, all but one of our properties is private and inaccessible outside the class. There is one read-only property nagging which uses a computed read-only value.

Most of these properties we’ll get to. We’ll start with the most important. The localNotification will contain our notification. Add the following method to present it immediately:

    func presentNotificationNow(){
        UIApplication.sharedApplication().presentLocalNotificationNow(localNotification)
    }

There are two ways to present notifications. We can present immediately as we do with the presentLocalNotificationNow method above or at some scheduled time in the future with the scheduleLocalNotification method. We could schedule that time using the fireDate property of UILocalNotification, but that presents a problem. We can only schedule 64 notifications, which in a nagging app might be too little space. We will take a different approach: Use a NSTimer loop in the background. Add the following method to start a timer:

func startTimer(timeInterval:NSTimeInterval){
        if !timer.valid{ //prevent more than one timer on the thread
            counter = 0
            backgroundTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler(nil)
            timer = NSTimer.scheduledTimerWithTimeInterval(
                timeInterval,
                target: self,
                selector: #selector(timerDidFire),
                userInfo: nil,
                repeats: true)
        }
        
    }

We do three things in this code. We reset a counter for counting how many nags the user has been subjected to, then set this task into the background. While on a simulator you can leave things in the background without this, your app will suspend operation without beginBackgroundTaskWithExpirationHandler on a live device. Finally we start a timing loop, getting the timing interval from the parameter timeInterval. The timing loop ends on a selector timerDidFire. Let’s add the code for timerDidfire. Add this to the class:

    func timerDidFire(timer:NSTimer){
            counter += 1
            let currentNag = counter % nags.count //cycle through messages
            let nagString = String(format:"# %i %@",counter,nags[currentNag])
            localNotification.alertBody = nagString
            localNotification.alertTitle = "Naggy Notification"
            localNotification.category = "Nags"
            localNotification.soundName = UILocalNotificationDefaultSoundName
            localNotification.userInfo = ["NagNum":counter,"NagString":nags[currentNag]]
            presentNotificationNow()
    }

If you are not familiar with timing loops you might want to refer to this lesson. Basically, the timer object is set to run the timerDidFire function every timeInterval seconds. Once in the timerDidFire function, we increment counter and compose a string we’ll post to the notification. The property alertBody of UILocalNotification sets the text for the notification. We also set the sound to the default sound, which will also set off vibrations and haptics. For later use, we all send a dictionary containing our count and our current nag as userInfo we can use later in this lesson. Finally we present the notification.

We’ll also need a way to shut down the timer. Add this method to your code:

func stopTimer(){
        timer.invalidate()
        UIApplication.sharedApplication().endBackgroundTask(backgroundTask)
}

We shut down the timer and remove the task from the background.

Now for the small, but insane bit of code. Add this above the class declaration:

let naggingNotification = NaggyNotification()

class NaggyNotification: NSObject {
...

A variable declared outside a class is global. Global values or instances can be used anywhere in the target code. There are other, better ways of handling the NaggyNotification class, but I could write full tutorials on them. As we’ll see, this instance will show up in several classes. This is a simple way to get it into all of them. As I always do when I use a global, I warn people not to use globals except in special circumstances. They are notoriously hard to track down in code. It’s why I made all the properties private, only methods are accessible to other classes, which are easier to deal with.

In order to use a notification, you must tell the application you will be using the notification in the AppDelegate.swift file. Go to the AppDelegate class Change the application:didFinishLaunchingWithOptions:launchOptions: method to this:

 func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        // Override point for customization after application launch.
        application.registerUserNotificationSettings(
            UIUserNotificationSettings(
                forTypes:[.Alert, .Sound],
                categories: nil
            )
        )
        return true
    }

We register the notification settings for the two types of notifications: Alerts(or banners) and sounds(or haptics and vibrations).

Our app has all the internal parts set, we need a user interface. We’ll make a very simple one of a single button. Go to the ViewController.swift file. Change ViewController to this:

class ViewController: UIViewController {  
    @IBOutlet weak var nagButton: UIButton!
    @IBAction func nagButton(sender: UIButton) {
    }
 }

We’ll have one button that we’ll assign a outlet and action to. Change the nagButton action to this:

@IBAction func nagButton(sender: UIButton) {
    if naggingNotification.nagging{
       naggingNotification.stopTimer()
    }else{
       naggingNotification.startTimer(10.0)
    }
      stateDidChange()
}

This code uses the naggingNotification read-only property nagging to determine if the timer is on. We toggle the timer depending on the result, using the naggingNotification methods stopTimer and startTimer. We’ve set up this demo with a 10 second timer.

Our last statement calls a method we have yet to define stateDidChange(). We’ll update the button title with this. Add this code to ViewController:

func stateDidChange(){
    if !naggingNotification.nagging{
       nagButton.setTitle"Nag", forState: .Normal)
    }else{
       nagButton.setTitle("Stop Nagging", forState: .Normal)
    }
}

We have our code. Go to the storyboard. Drag one button to the center of the storyboard. Title it Nag, and set the font to 46 point Bradley Hand.

2016-05-01_07-49-18

In the lower right corner of the storyboard, you will find the auto layout controls. With the Nag button selected, click the align alignment icon icon to get the align menu. Towards the bottom of this menu you will find the center Horizontally in Container and Vertically in container settings. Click both on with values of zero. Set the Update frames to Items of New constraints.

2016-04-26_15-32-25

Open the assistant editor. Drag from both circles in the code to the button to connect the action and the outlet to the button.  Close the assistant editor.

Before we test this, launch both the iPhone and watch simulators. If you have never launched these before running an app, check the directions here Especially with the watch, it helps to have them running before you run an app on them.

Build and run using a iPhone 6s plus simulator. You get the notification message the first time you run this:

2016-04-29_10-09-50

Press OK to allow notifications.You get a simple screen of a single button:

2016-05-01_07-54-37

Tap the Nag button.

2016-05-01_07-54-44

The button toggles but doesn’t do more than that. we need to be outside the app to see a notification.

Press Command-Shift-H or on the hardware simulator menu, Hardware>Home. Wait a few seconds and you will see this:

2016-04-29_10-09-53

Swipe down from the status bar once the banner disappears. Select Notifications and you will see the notifications appear.

2016-04-29_10-09-54

Tap one of the notifications and you are back in the app. Tap Stop Nagging and the notifications turn off.

Adding Categories and Actions to your Notifications

Introduced in iOS8, Notifications  now have their own actions, which appear as buttons on a notification. Often you’ll find these in the AppDelegate.  I’ll however keep everything on one class for the notification. Actions are of class UIUserNotificationAction and it’s subclass UIMutableUserNotificationAction. Since you can’t change anything in a UIUserNotificationAction, we define actions in UIMutableUserNotificationActions. We’ll define two actions, one to stop the timer and one to change the nag message to emoji. In the NaggyNotification class add the following method:

func makeActions()-> [UIMutableUserNotificationAction]{
    let stopAction = UIMutableUserNotificationAction()
    stopAction.title = "Stop"
    stopAction.identifier = "StopAction"
    stopAction.destructive = true
    stopAction.authenticationRequired = true
    stopAction.activationMode = .Foreground
     
    let moreAction = UIMutableUserNotificationAction()
    moreAction.title = "Emoji"
    moreAction.identifier = "EmojiAction"
    moreAction.destructive = false
    moreAction.authenticationRequired = false
    moreAction.activationMode = .Background
        
    return [stopAction,moreAction]
    }

The title parameter is the title that will appears on buttons of the notification. The identifier is the string used to internally identify the action. You’ll notice that there is no code here to execute the action. We’ll use that identifier to code the action in AppDelegate. We can add a red warning to a button if it is a destructive button, one that deletes something. I used it here as the Stop button, but left destructive as false for the Emoji button. The activationMode has two choices: .Foreground or .Background and indicates if the app will execute the action in the foreground or the background. To maintain security of the user’s phone we can also require an authentication before we execute the action. For actions in the foreground, this is mandatory and activationMode is always true. The statement in our code is redundant.

We return an array of the actions. We will use these actions in a category. Categories contain all information about a specific type of notification. Our category, which we’ll call Nags contains the actions that will show in the nag notification. Add the following code to the NaggyNotification class.

 //MARK: - Categories and Actions
    func makeCategory()-> UIMutableUserNotificationCategory {
        let category = UIMutableUserNotificationCategory()
        category.identifier = "Nags"
        category.setActions(makeActions(), forContext: .Default)
        return category
    }

We make set two properties on a UIMutableUserNotificationCategory. We set an identifier Nags and using the setActions:forContext: method add the actions to the category.

We’ve set up our category and actions. Now to use them. We’ll do that in AppDelegate. Find the method we changed earlier. Change the highlighted lines to add the category to our current code.

func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) ->; Bool {
        // Override point for customization after application launch.
        let category = naggingNotification.makeCategory()
        application.registerUserNotificationSettings(
            UIUserNotificationSettings(
                forTypes:[.Alert, .Sound, .Badge],
                categories: [category]
            )
        )
        return true
    }

If we ran our code now, we would have our actions listed in the notification, but nothing would happen. We need to add a callback method to AppDelegate containing the code to execute the actions. Add this to AppDelegate

    func application(application: UIApplication, handleActionWithIdentifier identifier: String?, forLocalNotification notification: UILocalNotification, completionHandler: () -> Void) {
        
        if notification.category == "Nags"{
            if identifier == "StopAction"{
                naggingNotification.stopTimer()
            }
            if identifier == "EmojiAction"{
                naggingNotification.toggleAlertMessages()
            }
        }
        completionHandler()
    }
  

The application:handleActionWithIdentifier:forlocalNotification method has a actionIdentifier and a UILocalNotification. for parameters.   The code checks the notification’s title. If it is our notification, we check the action identifier and perform the action we need.

We need one more method back in NaggyNotification to toggle the alert messages. Add this to the code.

func toggleAlertMessages(){
        if nags == basicNags {
            nags = emojiNags
        } else {
            nags = basicNags
        }
    }    

Fixing a Bug Before It Happens

This is all the code we need to run our application. However, there’s one bug that we’ll fix first. The problem is the Stop action when we are in the background. It will stop the the timer and shut down the nags. The button on our view controller does not know this. With the code as it is written the button still says Stop Nagging when we enter the foreground. We need to update the button any time we do enter the foreground. Find the applicationDidBecomeActive in AppDelegate. This method runs when we need to do that refresh of our user interface. Change it to this

    func applicationDidBecomeActive(application: UIApplication) {
        let rvc = window?.rootViewController as! ViewController
        rvc.stateDidChange() //Update the button
    }

Line 2 of the code gets the view controller. We have the view controller as our root view controller, so it’s easy to access. Once we have a reference to the root controller, we can call its method. statDidChange which checks the status of the timer and changes the title to the correct one.

Build and run.

2016-05-01_07-54-37

Press the nag button. Press Command-Shift-H. Eventually the notification appears:

2016-05-01_07-09-56

Drag the small oval at the bottom of the notification down.

2016-05-01_07-37-48

The actions appear.

2016-05-01_07-11-10

Tap the emoji button.  The next notification you get is an emoji.

2016-05-01_07-11-41

Drag down from the status bar and get the notifications. Swipe right to left on one of the notifications.

2016-05-01_07-15-52

Tap the Stop button. The app stops and opens up to the Nag screen again.

2016-05-01_07-54-37

The Apple Watch for All Developers

While that’s what you need to know for iOS, it’s not the whole story. Unless notifications are turned off by the user of an Apple watch, the watch will receive the notification if the phone is asleep.  Sadly, the watch simulator does not always reflect what happens on the watch.  You can try using the simulator if you  don’t have a watch handy, I’ll be using both since they have slightly different behaviors.

If your app is still running, get the watch and phone  simulator visible

2016-05-01_16-39-08

Start nagging with the phone, then press Command-L to Lock the phone. The simulator goes black. Wait a few seconds,  And you will see the Short-Look Notification appear:

Photo May 01, 9 26 23 AM

A second later, the  Long-Look notification appears.

2016-05-01_16-39-07

You’ll notice the buttons for the actions don’t quite fit on the watch. scroll up and you’ll find one more button.

2016-05-01_16-45-22

Test the emoji button.  Tap it, and the next notification changes to this:

2016-05-01_16-51-29

Press the stop button and you get a blank screen with the time. Actually, you are running a blank watch app.

2016-05-01_16-53-02

 You get slightly different  results with a real phone. Running the app on my phone, I start nagging by pressing the Nag button. I then lock my phone  by pressing the switch. SInce I turned on sounds and used the system sound in my actions, I’ll get a Haptic vibration on my wrist,  and then the notification.

Photo May 01, 9 25 40 AM

The notification includes the Emoji button and a dismiss button, which is cut off.  You can scroll up to get the rest of the dismiss button. On the simulator, we had the stop button, but on the watch it’s missing.

The difference has to do with activationMode. If you have an action with an activationMode of .Foreground the watch does not display it automatically. The foreground mode on the watch starts the watch app associated with this notification. In the simulator it lets us run that blank app, but on a physical watch it knows there is no app and doesn’t show the button.  On the other hand, an activationMode of .Background on the watch sends to the background of the phone. That’s why with no coding, the Emoji button works perfectly. Go to the NaggyNotification class. In makeActions, change our code to put the Stop button in the background

stopAction.activationMode = .Background

Now run the app again and when we lock the phone we get both buttons on the watch:

Photo May 01, 10 05 16 AM

With more buttons, the dismiss button is missing. Drag up and you will find it:

Photo May 01, 10 10 27 AM

Tap the Emoji Button. The next notification changes to emoji

Photo May 01, 10 05 31 AM

Tap the Stop button. On both the live watch and the simulator, the timer stops.  All of our actions work on the watch without a single new line of code.

Notifications will happen on an Apple Watch even if you don’t program for it. For Apple watch developer, this provides one way to launch your watch app. For iOS developers using notifications and actions, it means you have one more thing to think about when writing an app.  Think out what you want to show and not to show  for a watch user. If you are not writing a watch app, you can exclude actions from the watch but not the phone by making the  activationMode go to the foreground.

The whole code

NaggyNotification.swift

//
//  NaggyNotification.swift
//  PhoneWatchNotificationDemo
//
//  Created by Steven Lipton on 5/1/16.
//  Copyright © 2016 MakeAppPie.Com. All rights reserved.
//

import UIKit

//Global variable -- not always a good idea,
// but makes running this code much simpler
//
let naggingNotification = NaggyNotification()

class NaggyNotification: NSObject {
    
    //All properties are private or read only 
    //Good insurance for using the global instance
    //nothing gets messed up. 
    
    private let basicNags = ["Nag", "Nag nag","Naggity nag"]
    private let emojiNags = ["😀","😉","🙄","😒","😠","😡"]
    private var nags = ["Nag", "Nag nag","Naggity nag"]
    private var counter = 0
    private var timer = NSTimer()
    private var backgroundTask = 0
    private var localNotification = UILocalNotification()
    var nagging:Bool {return timer.valid } //read only
    
    func presentNotificationNow(){
        UIApplication.sharedApplication().presentLocalNotificationNow(localNotification)
    }
    func toggleAlertMessages(){
        if nags == basicNags {
            nags = emojiNags
        } else {
            nags = basicNags
        }
    }
    
    func startTimer(timeInterval:NSTimeInterval){
        if !timer.valid{ //prevent more than one timer on the thread
            counter = 0
            backgroundTask = UIApplication.sharedApplication().beginBackgroundTaskWithExpirationHandler(nil)
            timer = NSTimer.scheduledTimerWithTimeInterval(
                timeInterval,
                target: self,
                selector: #selector(timerDidFire),
                userInfo: nil,
                repeats: true)
        }
        
    }
    
    func timerDidFire(timer:NSTimer){
        counter += 1
        let currentNag = counter % nags.count //cycle through messages
        let nagString = String(format:"# %i %@",counter,nags[currentNag])
        localNotification.alertBody = nagString
        localNotification.alertTitle = "Naggy Notification"
        localNotification.category = "Nags"
        //localNotification.alertTitle = localNotification.category
        localNotification.soundName = UILocalNotificationDefaultSoundName
        localNotification.userInfo = ["NagNum":counter,"NagString":nags[currentNag]]
        presentNotificationNow()
        
    }
    
    func stopTimer(){
        timer.invalidate()
        UIApplication.sharedApplication().endBackgroundTask(backgroundTask)
    }
    
    //MARK: - Actions and Categories
    func makeActions()-> [UIMutableUserNotificationAction]{
        let stopAction = UIMutableUserNotificationAction()
        stopAction.title = "Stop"
        stopAction.identifier = "StopAction"
        stopAction.destructive = true
        stopAction.authenticationRequired = true
        stopAction.activationMode = .Background
        
        let moreAction = UIMutableUserNotificationAction()
        moreAction.title = "Emoji"
        moreAction.identifier = "EmojiAction"
        moreAction.destructive = false
        moreAction.authenticationRequired = false
        moreAction.activationMode = .Background
        
        return [stopAction,moreAction]
    }
    
    func makeCategory()-> UIMutableUserNotificationCategory {
        let category = UIMutableUserNotificationCategory()
        category.identifier = "Nags"
        category.setActions(makeActions(), forContext: .Default)
        return category
    }
}

AppDelegate.swift

//
//  AppDelegate.swift
//  PhoneWatchNotificationDemo
//
//  Created by Steven Lipton on 5/1/16.
//  Copyright © 2016 MakeAppPie.Com. All rights reserved.
//

import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?


    func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
        let category = naggingNotification.makeCategory()
        application.registerUserNotificationSettings(
            UIUserNotificationSettings(
                forTypes:[.Alert, .Sound],
                categories: [category]
            )
        )
        return true
    }
    
    func application(application: UIApplication, handleActionWithIdentifier identifier: String?, forLocalNotification notification: UILocalNotification, completionHandler: () -> Void) {
        
        if notification.category == "Nags"{
            if identifier == "StopAction"{
                naggingNotification.stopTimer()
            }
            if identifier == "EmojiAction"{
                naggingNotification.toggleAlertMessages()
            }
        }
        completionHandler()
    }
    

      func applicationDidBecomeActive(application: UIApplication) {
        let rvc = window?.rootViewController as! ViewController
        rvc.stateDidChange() //Update the button
    }

   
}



ViewController.swift

//
//  ViewController.swift
//  PhoneWatchNotificationDemo
//
//  Created by Steven Lipton on 5/1/16.
//  Copyright © 2016 MakeAppPie.Com. All rights reserved.
//

import UIKit


class ViewController: UIViewController {
    @IBOutlet weak var nagButton: UIButton!
    @IBAction func nagButton(sender: UIButton) {
        if naggingNotification.nagging{
            naggingNotification.stopTimer()
        }else{
            naggingNotification.startTimer(10.0)
        }
        stateDidChange()
    }
    func stateDidChange(){
        if !naggingNotification.nagging{
            nagButton.setTitle("Nag", forState: .Normal)
        }else{
            nagButton.setTitle("Stop Nagging", forState: .Normal)
        }
    }
}