Today, I want to talk about an Apple Framework: User Notifications.

Based on Apple's website, this is a simple Framework that allows us to push user-facing notifications to the user's device from a server, or generate them locally from our app.

I will not cover all the functionalities that this Framework provides, and instead just focus in in a simple problem that I had:

Send a periodically notification to the user only if some condition becomes true. For example, the notification that the Activity ring app (installed by default in all the Apple Watches) sends to the user every hour and 50 minutes if the user did not stand up in the last 50 minutes.

We can simplify the example like:

We want users do some task every day and alert them at 5 pm if they did not do the task yet. Basically, we can reset a bool variable at midnight and set it when the user does the task on the device, showing the notification at 5 pm if the variable is false.

Background

The easiest way to do this is running a python code on our server and send the remote notification to all the users that do not fit the criteria. The points here are:

  • Maybe the criterium depends on variables that we don't want to send to the server.
  • Users have all the information in their devices, why do we need to use extra resources?

To do it locally, in the device of the user, let's talk about how the Framework works.

Request a notification

First, we need to define the notification itself, providing the title, the body, the sound and some payload we want to include (using userInfo, attachments...):

let content = UNMutableNotificationContent()
content.title = "Hey dude, stand up!"
content.body = "The chair will not cry if you leave"
content.sound = UNNotificationSound.default

And then, prepare the trigger, which is how we say to the Framework when the notification should appear to our user. The Framework provides these kind of triggers:

  • UNCalendarNotificationTrigger: causes a notification to be delivered at a specific date and time.
  • UNTimeIntervalNotificationTrigger: causes a notification to be delivered after the specified amount of time elapses.
  • UNLocationNotificationTrigger: causes a notification to be delivered when the user's device enters or exits the specified geographic region.
  • UNPushNotificationTrigger: notification was sent from Apple Push Notification Service (APNs).

For example:

var date = DateComponents()
date.hour = 17
date.minute = 0 
let trigger = UNCalendarNotificationTrigger(dateMatching: date, repeats: true)

Using this trigger, we will present the notification to the user every day at 5:00 pm, no matter if the user did or not the task. Looking at the Framework documentation, there is not a way to combine different triggers (for example, at a specific date and time AND if the user's device enters or exists the specified geographic region) or run a code before showing the notification. All of this process is done by Apple OS without our intervention.

And here is when the problem arises: how do we say "push notification at 5 pm if the user did not do the task today"?

My solution

I spent some time looking for similar problems on Stackoverflow and I could not find a solution for this problem.

I played with the background modes. For example, run a piece of code at midnight and program the notification at 5 pm. If the user opens the app and does the task, I can remove the notification. This could work, but this Framework did not guarantee that my code runs when I want (for example, if the user did not charge the device and it is in low battery mode). What happens if the code did not run? or, what happens if the code run after the user does the task, but before 5 pm?

I decided the limitations I want to accept for my solution:

  • I want to run everything on the local device.
  • If the user did not open the app, I want to send the notification.
  • If the user opens the app, but did not do the task, I want to send the notification.
  • If the user opens the app and did the task, I don't want to send the notification today.

I marked another limitation:

  • If the user did not open the app for a week, I will stop bothering them.

Based on these limitations, I prepared a code that programs the next seven notifications without repetitions:

let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd"
let now = Date()
let today = Calendar.current.startOfDay(for: now)
for dayInterval in (includeToday ? 0 : 1)...7 {
    guard
        let currentDay = Calendar.current.date(byAdding: .day, value: dayInterval, to: today),
        let notificationDate = Calendar.current.date(bySettingHour: type.hour, minute: type.minute, second: type.second, of: currentDay)
    else { continue }

    let timeInterval = notificationDate.timeIntervalSince(now)
    if timeInterval < 0 {
        // The time passed
        continue
    }
    let trigger = UNTimeIntervalNotificationTrigger(timeInterval: timeInterval, repeats: false)
    let identifier = "SOME-USEFUL-PREFIX$\(formatter.string(from: currentDay))"

    let content = UNMutableNotificationContent()
    content.title = "Hey dude, stand up!"
    content.body = "The chair will not cry if you leave"
    content.sound = UNNotificationSound.default

    let request = UNNotificationRequest(identifier: identifier, content: content, trigger: trigger)
    center.add(request) { error in
        if let error = error {
            print("Error at \(identifier): \(error.localizedDescription)")
        } else {
            print("Notification added \(identifier)!")
        }
    }
}

And then, if the user does the task, I can finally remove the pending notification:

let now = Date()
let identifier = "SOME-USEFUL-PREFIX$\(formatter.string(from: now))"
center.removePendingNotificationRequests(withIdentifiers: [identifier])

This is the solution I thought, but, for sure, it will be a better one. Do you want to share yours?