Short Repeating Notifications in iOS

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.2017-02-27_06-16-49

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 30s2017-02-24_05-47-53

Go to the storyboard. Drag out two buttons and a label. Title and arrange them like this:

2017-02-24_05-53-15

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. 2017-02-24_06-06-31

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:

2017-02-24_07-30-38

Tap Allow.  Hit the Start button and you immediately get a notification

2017-02-24_07-31-57

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.

2017-02-24_09-34-15

Select Allow, and then Start. Notifications will start to appear every ten seconds.

2017-02-24_09-24-54

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.

2017-02-24_09-35-36

Swipe toward the left and you’ll see two buttons

2017-02-24_09-35-54

Tap the View button.  You’ll see your notification and the action button. Tap Stop and the notifications stop.

2017-02-24_09-36-11

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

2017-02-24_09-49-41

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

}


2 Replies to “Short Repeating Notifications in iOS”

  1. Hi Steven. You might want to try this on a real device. You’ll find that timers stop running once your app goes into the background, breaking your app :-(

    Why timers keep running in the simulator is anyone’s guess.

    Geoff.

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