In an earlier post, I discussed the repeating local notifications in the UserNotification
framework, but that discussion misses one detail: repeating notifications less than 60 seconds. The framework will very stubbornly refuse to have a repeating notification less than a minute, giving a runtime error if you do.
In my LinkedInLearning course Learning iOS Notifications, I mentioned a workaround to get around this problem, but didn’t show how to in that course. Today, I’ll show you how to get repeating notifications for times less than 30 seconds.
Before I do, I will give a big warning from the user experience perspective. Rapid-fire notifications are annoying to the user under most circumstances. Don’t do this unless the functionality of the application is such that the user needs the frequent interruptions. Almost all the cases I can think of are interval timers.
In fitness, interval training is a recommended training regimen with lots of benefits . There’s even a movement created by U.S. Olympian Jeff Galloway among distance runners to do run/walk/run a interval-based distanced running strategy to reduce injury. There’s also speedwork intervals for runners. It’s here that you’ll often see those less than 60 second time intervals repeated. Often it will be 30 seconds of intense activity repeated with lesser activity for a rest period. In this lesson, We’ll set up a 30 second repeat timer to show you the workaround to the 60 second limitation.
I’m going to assume you have a good idea of how to use local notifications for this lesson. If you don’t, try this lesson first or even better take my video course Learning iOS Notifications on Lynda.com or LinkedInlearning.
Make a New Project
I’ve made a starting file for this lesson which you can download here: shortrepeat_start If making this yourself, Make a new single view project ShortRepeat. When the project loads change the display name to Repeat 30s
Go to the storyboard. Drag out two buttons and a label. Title and arrange them like this:
You can leave this like this for this lesson if you want to code. I’ll clean them up in a little auto layout and color:
To see how I did this, you can download the start file.
Open the assistant editor and add two actions to the code:
@IBAction func startButton(sender:UIButton) { } @IBAction func stopButton(sender:UIButton) { }
Drag from the startButton
‘s circle to the to the Start button. Drag from the stopButton
‘s circle to the Stop button.
You’re ready to code. You can download a starter version of this here: shortrepeat_start
Set Up a Notification
There’s a few steps we’ll need to set up before using a notification. Close the assistant editor and go to the ViewController
class code. You’ll need to include the UserNotification
framework first. Under import UIKit
, add this
import UserNotifications
You’ll be using the user notification delegate too. Add its protocol to the class
class ViewController: UIViewController,UNUserNotificationCenterDelegate {
At the bottom of the class, add this to make the two delegates:
//MARK: - User Notification Center Delegates func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { completionHandler() } func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.alert,.sound]) }
The usernotificationCenter:willPresent notification
method assures us that the notification will show up and sound the alert when we are viewing the application. We’ll use the usernotificationCenter:didRecieve response:
a little later to code a stop action, but for now I added the required completion handler.
Go to ViewDidload
to point the delegate to the delegate methods.
UNUserNotificationCenter.current().delegate = self
Notifications require user permission, so add this to viewDidLoad
as well:
UNUserNotificationCenter.current().requestAuthorization( options: [.alert,.sound]) {(granted, error) in self.isGrantedAccess = granted }
You’ll set the notification for alert and sound, and set a variable isGrantedAccess
as a flag to decide if you have been granted access for notifications.
Of course you haven’t defined isGrantedAccess
yet. Add it to the top of the class.
var isGrantedAccess = false
You’ll also need a category with a Stop button action for the notification. Add this to viewDidLoad
let stopAction = UNNotificationAction(identifier: "stop.action", title: "Stop", options: []) let timerCategory = UNNotificationCategory(identifier: "timer.category", actions: [stopAction], intentIdentifiers: [], options: []) UNUserNotificationCenter.current().setNotificationCategories([timerCategory])
This adds a stop action, which will display a stop button on the notification.
Making a Notification
We’ve done much of the setup, it’s time to make the notification. The secret to the notification is to make a time interval trigger of near zero, to launch an immediate notification. You’ll use a timer to send the notification.
Make a new function sendNotification
. Add an if
clause to check if we are granted access
func sendNotification(){ if isGrantedAccess{ } }
In the if
clause’s block, add some content:
let content = UNMutableNotificationContent() content.title = "HIIT Timer" content.body = "30 Seconds Elapsed" content.sound = UNNotificationSound.default() content.categoryIdentifier = "timer.category"
This will print a simple message and will play the default sound when 30 seconds is up. It also will use the timer.category
to display buttons.
Next add the trigger. The strategy is to use a trigger at zero, but a timeInterval
of zero is not allowed either. Use a time interval trigger set to a small interval such as 0.001, with no repeat to send one notification immediately.
let trigger = UNTimeIntervalNotificationTrigger( timeInterval: 0.001, repeats: false) //close to immediate as we can get.
Finally make and add the notification request to the current user notification center. For error handling, I just print a message to the console.
let request = UNNotificationRequest(identifier: "timer.request", content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) { (error) in if let error = error{ print("Error posting notification:\(error.localizedDescription)") } }
To test out this function, add it to the startButton
method
@IBAction func startButton(_ sender: UIButton) { sendNotification() }
Build and Run. you’ll get the permissions alert:
Tap Allow. Hit the Start button and you immediately get a notification
Repeat the Notification
I’ve gotten the notification to fire pretty close to immediately. Now I’ll add a timer to fire repeatedly every 10 seconds. Add to the top of the view controller class a timer
private var timer = Timer()
Code the following function to start a timer:
func startTimer(){ let timeInterval = 10.0 if isGrantedAccess && !timer.isValid { //allowed notification and timer off timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true, block: { (timer) in self.sendNotification() }) } }
For this code, I used the Timer
initializer with a closure named block
, instead of the one with a target. We have only one line of code to send the notification, to execute every time the timer completes an interval, so using a closure is more compact and easier to read than calling another function. I placed the timer’s start in a if
clause to prevent the timer from starting if we don’t have permission for notifications and if the timer is already started. We don’t want to re-start the timer once started because that will mess up the intervals we are timing, and it is possible for someone working out to accidentally hit the Start button on their phone. I also set the time interval to a shorter ten seconds for testing purposes. I’ll move it back to 30 as shown in the notification when the app finishes testing.
Stopping the Notification
I’ll need to stop the notifications firing when I’m done with my workout. To do that, I’ll make another function to invalidate the timer. I’ll also clean out all the notifications
func stopTimer(){ //shut down timer timer.invalidate() //clear out any pending and delivered notifications UNUserNotificationCenter.current().removeAllPendingNotificationRequests() UNUserNotificationCenter.current().removeAllDeliveredNotifications() }
There’s nothing useful about having a timer notifications floating around, so getting rid of them when you stop the timer makes things nice and tidy. There can be at most two here, because all pending notifications replace each other. Therefore you may have one pending notification, and very likely will have one delivered notification at most.
I’ll place the stopTimer
method in two places. First, I’ll place it in the stopButton
@IBAction func stopButton(_ sender: UIButton) { stopTimer() }
Secondly, I’ll add it as the action for the stop.action
on the notification. Drop down to the user notification center delegates at the bottom of your code and change the userNotificationCenter: didReceive response:
to this:
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { if response.actionIdentifier == "stop.action"{ stopTimer() } completionHandler() }
Set your simulator device to an iPhone6. Phones later than this one use 3D touch in the simulator and you may not be able to get the action to appear otherwise with mice and older trackpads in the simulator. Build and run.
Unless you were using an iPhone 6 simulator earlier in this tutorial, the phone should boot and then ask permission again.
Select Allow, and then Start. Notifications will start to appear every ten seconds.
Press Stop The notifications stop. Press Start again and then Command-L to lock the phone. You’ll start to see notifications on the lock screen.
Swipe toward the left and you’ll see two buttons
Tap the View button. You’ll see your notification and the action button. Tap Stop and the notifications stop.
Bonus:The Apple Watch and Repeat Notifications
The Stop action button is something you should definitely put into your application. On a long run, it takes a while to unlock the phone and hit Stop in the app. Opening the notification is faster. There is the other place this notification might show up: An Apple Watch
Local phone notifications appear on an Apple watch paired to a phone that is locked or asleep. There may be a slight time delay, but they will show up. On a workout where you start your app, the phone will eventually lock during the workout. The paired Apple watch will continue to get the notifications. Since we included sound as part of the content, that will mean the watch will include a haptic touch for the notification.
This method of repeat notification does have it limitations. It does run in the foreground and does use more processor time and power than the repeat notifications. You’ll need to set it up for background timing if you turn the app off or the device times out. If you do not have to do time intervals less than 60 seconds use the framework instead of this.
The Whole Code
You will find the complete project here: shortrepeat_end It includes some extra watchOS files and some graphics to make the watch and project look a little nicer.
// // ViewController.swift // ShortRepeat // // Created by Steven Lipton on 2/24/17. // Copyright © 2017 Steven Lipton. All rights reserved. // import UIKit import UserNotifications class ViewController: UIViewController,UNUserNotificationCenterDelegate { var isGrantedAccess = false private var timer = Timer() func startTimer(){ let timeInterval = 30.0 if isGrantedAccess && !timer.isValid { //allowed notification and timer off timer = Timer.scheduledTimer(withTimeInterval: timeInterval, repeats: true, block: { (timer) in self.sendNotification() }) } } func stopTimer(){ //shut down timer timer.invalidate() //clear out any pending and delivered notifications UNUserNotificationCenter.current().removeAllPendingNotificationRequests() UNUserNotificationCenter.current().removeAllDeliveredNotifications() } func sendNotification(){ if isGrantedAccess{ let content = UNMutableNotificationContent() content.title = "HIIT Timer" content.body = "30 Seconds Elapsed" content.sound = UNNotificationSound.default() content.categoryIdentifier = "timer.category" let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 0.001, repeats: false) let request = UNNotificationRequest(identifier: "timer.request", content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) { (error) in if let error = error{ print("Error posting notification:\(error.localizedDescription)") } } } } @IBAction func startButton(_ sender: UIButton) { startTimer() } @IBAction func stopButton(_ sender: UIButton) { stopTimer() } override func viewDidLoad() { super.viewDidLoad() UNUserNotificationCenter.current().delegate = self UNUserNotificationCenter.current().requestAuthorization(options: [.alert,.sound]) { (granted, error) in self.isGrantedAccess = granted } let stopAction = UNNotificationAction(identifier: "stop.action", title: "Stop", options: []) let timerCategory = UNNotificationCategory(identifier: "timer.category", actions: [stopAction], intentIdentifiers: [], options: []) UNUserNotificationCenter.current().setNotificationCategories([timerCategory]) } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() // Dispose of any resources that can be recreated. } //MARK: - User Notification Center Delegates func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { if response.actionIdentifier == "stop.action"{ stopTimer() } completionHandler() } func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { completionHandler([.alert,.sound]) } }
Leave a Reply