How to Repeat Local Notifications

I’ve done several past posts about local and remote notifications using the new UserNotifications framework. One of the new features of the new framework is tiny, but  has a big impact: repeat notifications. While you used to need timer loops to make notifications they become insanely easy in most cases to get a notification to repeat. I also glossed over calendar triggers in other lessons. In this lesson, I’ll show you how to make a repeating calendar based trigger, and show you that while making a trigger might be easy, stopping one is not so simple.

Along the way I’ll give you a few more tips about local notifications.

Make the Project

Unlike my earlier notification projects, I’m going to start from scratch on this one.  Make a new project called RepeatNoificationDemo with Swift for the Language and a Universal device.  On the storyboard add two buttons and a label like this:

2017-01-29_09-00-06

In the assistant editor make two actions from the buttons: add notification and showNotificationStatus.

At the top of the viewController.swift code, just under Include UIKit add this:

include UserNotifications

Check for Permission

With any notification app you must check for permission. This must be one of the first things you do in an application. There’s two places you can check this permission: one is in the App delegate, the other in the root or initial view controller. The earlier you do this the better. With a remote notification, place this in the app delegate. For a local notification, place this in the root view controller. There’s a reason for this: For a local notification the system will think the user did not grant permission on the first run of the application. the AppDelegate and View controller don’t run sequentially. The app delegate will get one answer and the view controller will get another, confusing everything. For local notifications avoid this and place the permission in viewDidLoad like this:

override func viewDidLoad() {
    super.viewDidLoad()
    UNUserNotificationCenter.current().requestAuthorization(
        options: [.alert,.sound]) 
        { 
            (granted, error) in
            self.notificationGranted = granted
            if let error = error {
                print("granted, but Error in notification permission:\(error.localizedDescription)")
        }
}  

You’ll need notificationGranted as a property of the viewController class, so add this:

var notificationGranted = false

Make the Notification Content

I’ll make a function to add the notification to the notification center. Above viewDidLoad add at the following function:

func repeatNotification(){
}

To the function add the following content.

let content = UNMutableNotificationContent()
content.title = "Pizza Time!!"
content.body = "Monday is Pizza Day"
content.categoryIdentifier = "pizza.reminder.category"

This gives us a notification with a title Pizza Time!! and the message Monday is Pizza Day

How to Easily Repeat a Notification

Repeating a notification is easy. You can repeat either a TimeInterval or a Calendar Notification. Both have a Bool parameter repeats. If you set this to true, you get a repeating notification. However the repetitions must be 60 seconds or more apart. This is the smallest time interval repeating trigger:

let  trigger = UNTimeIntervalNotificationTrigger(
    timeInterval: 60.0,
    repeats: true)

I’m not going to add a time interval to the demo. Instead I’ll add a calendar notification. Calendar notifications use DateComponents objects to specify the date. For a repeating notification, add the components you need and nothing else. for example to set data components to Monday at 11:30 AM add this

var dateComponents = DateComponents()
// a more realistic example for Gregorian calendar. Every Monday at 11:30AM
dateComponents.hour = 11
dateComponents.minute = 30
dateComponents.weekday = 2
ddateComponents.second = 0

DateComponents are just numbers, dependent on the calendar they are associated with. weekday in this example may be Monday or Tuesday depending if Sunday is the first day of the week in the calendar. I’m using a U.S. Gregorian calendar so it is Monday. While this is how you set up a calendar event, this is difficult to test or demo. So comment out the hour, minute and weekday to run the example at the top of the minute.

//Date component trigger
var dateComponents = DateComponents()
// a more realistic example for Gregorian calendar. Every Monday at 11:30AM
//dateComponents.hour = 11
//dateComponents.minute = 30
//dateComponents.weekday = 2
// for testing, notification at the top of the minute.
dateComponents.second = 0

Make a repeating calendar trigger like this:

let trigger = UNCalendarNotificationTrigger(
    dateMatching: dateComponents, 
    repeats: true)

Add the Request

Add the request like any other local notification. Add the following code:

        let request = UNNotificationRequest(identifier: "pizza.reminder", content: content, trigger: trigger)
        
        UNUserNotificationCenter.current().add(request) { (error) in
            if let error = error {
                print("error in pizza reminder: \(error.localizedDescription)")
            }
        }
        print("added notification:\(request.identifier)")
 

For explanation see the local notification lesson I posted earlier.

Now you can use the function to schedule the notification. Modify the addNotification action to this to check for user permission. If so, add the notification:

 @IBAction func addNotification(_ sender: UIButton) {
        if notificationGranted{
            repeatNotification()
        }else{
            print("notification not granted")
        }
        
    }

Adding the In-App Presentation Delegate

You are ready to run the code to make the notification, but I like to add a few more things thing to the application before I do: The UNUserNotificationCenterDelegate. Once again, you can add it to the root view controller or the app delegate. For this, I’ll habitually add it to the app delegate, because it should be run as soon as possible. Go to the appDelegate.swift file. Adopt the protocol:

class AppDelegate: UIResponder, UIApplicationDelegate,UNUserNotificationCenterDelegate {

Change the application:didFinishLaunching to point to the delegate methods:

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
// Override point for customization after application launch.
    UNUserNotificationCenter.current().delegate = self
    return true
 }

Add the following delegate method at the bottom of the class

    //MARK: - Delegates
    func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
        completionHandler([.alert,.sound])
        
    }

This delegate can do a lot, but its core is the completion handler. You specify how you want notifications to appear if you are in the app generating them. The code completionHandler([.alert,.sound]) shows a notification and gives a sound if in the app. Usually this is the current default behavior, so if you don’t use this delegate method at all, you’ll get this behavior. If you add it and don’t set completionHandler, or set completionHandler to empty options, you suppress the notification in-app.

Checking pending notifications

As I discussed in more detail in another post, The UserNotifications framework can monitor pending notifications, the notifications waiting to appear. Theres a UNUserNotificationCenter method to check this. The getPendingNotificationRequests method uses a closure to handle the collection of requests it returns. To list them, iterate through them with a for loop.

@IBAction func showNotificationStatus(_ sender: UIButton) {
    var displayString = "Current Pending Notifications "
    UNUserNotificationCenter.current().getPendingNotificationRequests {
    (requests) in
        displayString += "count:\(requests.count)\t"
        for request in requests{
            displayString += request.identifier + "\t"
         }
         print(displayString)
}

This prints a single line in the console to tell you how many notifications are pending, and what they are by their request identifier specified in the UNNotificationRequest object.

Stopping a Notification

I’d be tempted to run right now, but there’s one little problem: Stopping the notification. The code to stop a pending notification is simple. The UNUserNotificationCenter object does have a removePendingNotificationRequests method. If I were to use it literally it would looks like this:

UNUserNotificationCenter.removePendingNotificationRequests(withIdentifiers: ["pizza.reminder"])

That’s all you need. You specify the identifier of the pending notification. If there, the center removes the pending notification. However there’s two problems with this: When do you remove the repeating notification and where in code do you do this?

A Stop Action

One answer to both of these questions is when a user says so. With repeating notifications, this is always a good idea, so your notification never gets annoying, and the user dumps the app or shuts off notifications. You can code an action to stop repeating. I tend to code my actions in the AppDelegate. Go to the AppDelegate.swift file.
Add a function setCategories in the AppDelegate class

func setCategories(){
}

Actions in notifications are pointers to some code that runs later in a delegate. You store a collection of actions in a category, and add the categories to the notification center. You’ll see above we set in the content of the notification a categoryIndentifier. When the notification presents, the notification center checks this identifier against the categories it has listed. If it exists, it turns the actions in the category into buttons on the notification.
In the function make the action using the UNNotificationAction constructor.

let clearRepeatAction = UNNotificationAction(
   identifier: "clear.repeat.action",
   title: "Stop Repeat",
   options: [])

This has three parameters: the identifier of the action, a title for the button and some options we’ll ignore here. You’ll come back to this identifier shortly.Under that, add the category, adding the action you just defined as part of the category.

let pizzaCategory = UNNotificationCategory(
    identifier: "pizza.reminder.category",
    actions: [clearRepeatAction],
    intentIdentifiers: [],
    options: [])
}

The category constructor also contains an indentifier, the one that must match some categoryIdentifier in a notification’s content. The actions parameter can  list up to four actions in an array. The intent identifiers has to do with Siri, and again we’ll skip it and the options.
Finally add the category to the notification center.

UNUserNotificationCenter.current().setNotificationCategories([pizzaCategory])

The function should look like this:

func setCategories(){
    let clearRepeatAction = UNNotificationAction(
        identifier: "clear.repeat.action",
        title: "Stop Repeat",
        options: [])
    let pizzaCategory = UNNotificationCategory(
        identifier: "pizza.reminder.category",
        actions: [clearRepeatAction],
        intentIdentifiers: [],
        options: [])
    UNUserNotificationCenter.current().setNotificationCategories([pizzaCategory])
}

Then add setCategories to the application:didFinishLaunchingWithOptions method.

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.
        UNUserNotificationCenter.current().delegate = self
        setCategories()
        return true
    }

That defines the action but does nothing. Go to the bottom of the code. You’ll add the second UNUserNotificationCenterDelegate method under the first one for in-app notifications.

func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
        print("Did recieve response: \(response.actionIdentifier)")
        if response.actionIdentifier == "clear.repeat.action"{
            UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [response.notification.request.identifier])
        }
        completionHandler()
    }

This method fires when a user selects some action in a notification. The UNNotificationResponse object response contains the actionIdentifier and the UNNotification object for the user interaction. This code is for any notification that uses this action. You are not restricted to using one action to one notification. It checks if the actionIdentifier is the correct one for removing the pending notification. If it is, the center removes it, using the request. At the end of this method you must call the completionHandler.

You can build and run now. You’ll first be asked if the user grants permission to use notifications:

2017-01-30_08-15-22

Then you have the the main screen. Add a notification.

2017-01-30_08-09-26

At the top of the minute, you’ll get the notification:

2017-01-30_08-10-19

You can wait a few minutes and see the notification repeat. You can tap the Notification Status button and see the pending notification:

added notification:pizza.reminder
Current Pending Notifications count:1 pizza.reminder

When you are done, Swipe down on the notification to view the full notification:

2017-01-30_08-13-11

Tapping the Stop Repeat button stops the notification.

Using userInfo

Besides the user removing the repeat notification, when does your code remove the pending notification automatically? You’ll need some time when you end the notifications. You’ll store that somewhere, compare it to the current date, and if the current date is past the end date, remove the notification. It’s best to store the end date in the notification itself. For this you’ll use another content property of UNUserNotificationContent. The userInfo property is a dictionary where you store whatever you want. It may be extra content, or control values for the notification. for example, I could store a date 90 days into the future like this:

// Set the end date to a number of days
let dayInSeconds = 86400.0
content.userInfo["endDate"] = Date(timeIntervalSinceNow: dayInSeconds * 90.0)       

This uses a constant of the number of seconds in a day, and multiplies it by 90 days. Using the Date:TimeIntervalSinceNow method, we add a date 90 days from now to a new dictionary entry endDate.
Again that’s hard to test, so I’ll set this for a minute from the time the notification is created. Add this to your repeatNotification function under the content.categoryIdentifier line.

content.userInfo["endDate"] = Date(timeIntervalSinceNow: 60.00)

You’ve stored the notification’s end date in the notification. Now you need to compare it to the current date.

An Extension to Clean Repeating Notifications

As you’ll see there some issues about where in the code we should try killing the notification. For that reason, I’ll create a new function, extending the UNNotificationCenter class so I can use this function anywhere in my code.

You might not have used an extension before, so it needs a small amount of explanation what I’m about to do. Swift contains a structure called an extension which tacks on more properties and methods to already existing classes, especially factory ones. Go to the ViewController.swift file just below the import UserNotifications line. Add the following:

extension UNUserNotificationCenter{

}

This adds an extension to the UNNotification center. You add methods to this extension, and then use the method as if it is in the class you extended. Add the following method to the extension:

extension UNUserNotificationCenter{
    func cleanRepeatingNotifications(){
    }
}

This will add the function cleanRepeatingNotifications to the UNUserNotificationCenter. In this function we’ll get the pending notifications, look at them and see if any have and expired date. If they do, we’ll remove them form pending notifications. First you get the list of pending notifications in a array called requests.

extension UNUserNotificationCenter{
    func cleanRepeatingNotifications(){
        //cleans notification with a userinfo key endDate
        //which have expired.
        var cleanStatus = "Cleaning...."
        getPendingNotificationRequests {
            (requests) in
       }
    }
}

The array exists in a closure. Add code in the closure to iterate through the requests, cleaning as you go.

for request in requests{
    if let endDate = request.content.userInfo["endDate"]{
         if Date() >= (endDate as! Date){
              cleanStatus += "Cleaned request"
              let center = UNUserNotificationCenter.current()
              center.removePendingNotificationRequests(
                   withIdentifiers: [request.identifier])          
         } else {
              cleanStatus += "No Cleaning"
         }
         print(cleanStatus)
     }
}

I want to point out two lines in this code.

    if let endDate = request.content.userInfo["endDate"]{
         if Date() >= (endDate as! Date){

This is how we get to the user dictionary. I used if let optional chaining so if there is an endDate entry, the function checks if it needs to clean, otherwise it skips the process. if it does need to clean, userInfo‘s value is of type Any, so I downcast it to Date in the comparison afterwards.
The complete extension looks like this:

extension UNUserNotificationCenter{
    func cleanRepeatingNotifications(){
        //cleans notification with a userinfo key endDate
        //which have expired.
        var cleanStatus = "Cleaning...."
        getPendingNotificationRequests {
            (requests) in
            for request in requests{
                if let endDate = request.content.userInfo["endDate"]{
                    if Date() >= (endDate as! Date){
                        cleanStatus += "Cleaned request"
                        let center = UNUserNotificationCenter.current()
                        center.removePendingNotificationRequests(
                             withIdentifiers: [request.identifier])
                    } else {
                        cleanStatus += "No Cleaning"
                    }
                    print(cleanStatus)
                }
            }
        }
    }    
}

Where to Put cleanRepeatingNotifications

Here’s a big question that makes repeat notifications so difficult: where do you put cleanRepeatingNotifications? Ideally, you would would run it every time the notification gets delivered. But there’s no way to write code there — at least of this writing. Push notifications have the service extension to do that, but local notifications don’t have that mechanism. Of course you don’t need any of this for a push notification because the repetition and termination of the repetition is set on the server, not your device.
Since we have no background place to call it, You have to call it in a few places that users interact with the application, and hope they hit one of those. In each you’d add the line

UNUserNotificationCenter.current().cleanRepeatingNotifications()

I haven’t yet found a good place that works 100%. A user can keep getting these messages if they never interact with the app or the notification. I’d suggest the following places to add this code:

  • On launch. You can add this to View did load in the initial view controller, or in the app delegate  method application.
  • On interaction with the notification. Add to the delegate method userNotificationCenter:DidRecieveResponse.
  • On loading a custom notification layout from the Content Extension. I covered Content extensions last time, but when a user presents the full content of a custom notification, you can check the notification for expiration. I gave an example in the download file and whole code, which directly cleans the current notification. Setting up here would take too long.

I’d love to have code in the background do this, but I haven’t figured out how.

The Whole Code

I mentioned up above there is a download file for this project you can get here: repeatnotificationdemo. I added a content extension to this project’s download file and activated it, so the notification will look like this in the download compared to the project described above.

2017-01-31_07-06-44

AppDelegate.swift

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

import UIKit
import UserNotifications

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate,UNUserNotificationCenterDelegate {

    var window: UIWindow?
    
    func setCategories(){
        let clearRepeatAction = UNNotificationAction(
            identifier: "clear.repeat.action",
            title: "Stop Repeat",
            options: [])
        let pizzaCategory = UNNotificationCategory(
            identifier: "pizza.reminder.category",
            actions: [clearRepeatAction],
            intentIdentifiers: [],
            options: [])
        UNUserNotificationCenter.current().setNotificationCategories([pizzaCategory])
    }

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        UNUserNotificationCenter.current().delegate = self
        setCategories()
        UNUserNotificationCenter.current().cleanRepeatingNotifications()
        return true
    }

    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) {
        UNUserNotificationCenter.current().cleanRepeatingNotifications()
        print("Did recieve response: \(response.actionIdentifier)")
        if response.actionIdentifier == "clear.repeat.action"{
            UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [response.notification.request.identifier])
        }
        completionHandler()
    }
}

ViewController.swift

//
//  ViewController.swift
//  RepeatNotificationDemo
//
//  Created by Steven Lipton on 1/25/17.
//  Copyright © 2017 Steven Lipton. All rights reserved.
//

import UIKit
import UserNotifications

extension UNUserNotificationCenter{
    func cleanRepeatingNotifications(){
        //cleans notification with a userinfo key endDate
        //which have expired.
        var cleanStatus = "Cleaning...."
        getPendingNotificationRequests {
            (requests) in
            for request in requests{
                if let endDate = request.content.userInfo["endDate"]{
                    if Date() >= (endDate as! Date){
                        cleanStatus += "Cleaned request"
                        let center = UNUserNotificationCenter.current()
                        center.removePendingNotificationRequests(withIdentifiers: [request.identifier])
                    } else {
                        cleanStatus += "No Cleaning"
                    }
                    print(cleanStatus)
                }
            }
        }
    }

    
}

class ViewController: UIViewController {
    var notificationGranted = false
    
   
    @IBAction func addNotification(_ sender: UIButton) {
        if notificationGranted{
            repeatNotification()
        }else{
            print("notification not granted")
        }
        
    }
    
    @IBAction func showNotificationStatus(_ sender: UIButton) {
        var displayString = "Current Pending Notifications "
        UNUserNotificationCenter.current().getPendingNotificationRequests {
            (requests) in
            displayString += "count:\(requests.count)\t"
            for request in requests{
                displayString += request.identifier + "\t"
            }
            print(displayString)
        }
        
        //cleanNotifications()
        
    }
    
    
    func repeatNotification(){
        let content = UNMutableNotificationContent()
        content.title = "Pizza Time!!"
        content.body = "Monday is Pizza Day"
        content.categoryIdentifier = "pizza.reminder.category"
        
        //for testing
        content.userInfo["endDate"] = Date(timeIntervalSinceNow: 60.00)
        
        // Set the end date to a number of days
        //let dayInSeconds = 86400.0
        //content.userInfo["endDate"] = Date(timeIntervalSinceNow: dayInSeconds * 90)
        
        //A repeat trigger for every minute
        //You cannot make a repeat shorter than this.
        //let  trigger = UNTimeIntervalNotificationTrigger(timeInterval: 60.0, repeats: true)
        
        //Date component trigger
        var dateComponents = DateComponents()
        
        // a more realistic example for Gregorian calendar. Every Monday at 11:30AM
        //dateComponents.hour = 11
        //dateComponents.minute = 30
        //dateComponents.weekday = 2
        
        // for testing, notification at the top of the minute.
        dateComponents.second = 0
        
        let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
        
        let request = UNNotificationRequest(identifier: "pizza.reminder", content: content, trigger: trigger)
        
        UNUserNotificationCenter.current().add(request) { (error) in
            if let error = error {
                print("error in pizza reminder: \(error.localizedDescription)")
            }
        }
        print("added notification:\(request.identifier)")
    }
    
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        UNUserNotificationCenter.current().requestAuthorization(options: [.alert,.sound]) { (granted, error) in
            self.notificationGranted = granted
            if let error = error {
                    print("granted, but Error in notification permission:\(error.localizedDescription)")
            }
        }
        UNUserNotificationCenter.current().cleanRepeatingNotifications()
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        //UNUserNotificationCenter.current().cleanRepeatingNotifications()
    }

    
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }


    
}


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s