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:
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:
Then you have the the main screen. Add a notification.
At the top of the minute, you’ll get the notification:
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:
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.
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