Scheduling daily notifications on iOS using Calendar and DateComponents

Published on: December 12, 2019

On iOS, there are several ways to send notifications to users. And typically every method of sending push notifications has a different goal. For example, when you're sending a remote push notification to a user, you will typically do this because something interesting happened outside of the user's device. Somebody might have sent them a message for example, or something exciting happened during a sports game.

However, when we schedule notifications on the device locally, we typically do this as a reminder, or in response to a user action. Like, for example, entering or exiting a geofence. In today's post, I'm going to focus on scheduling notifications that are essentially repeating reminders. They are notifications that are scheduled to show up every day at a certain time, or maybe every Monday at 09:00 am, or on every first day of the month. The same technique can be used to schedule each of these notifications.

In today's post you will learn about the following:

  • Working with the Calendar and DateComponents.
  • Scheduling notifications using a UNCalendarNotificationTrigger.

Let's get started with learning about the Calendar, shall we?

Working with the Calendar and DateComponents

If you have worked extensively with dates and times on a global scale, you know that pretty much any assumptions you may have about how dates work are wrong. A good way to explore how complicated dates can be is by playing with the Calendar object that is part of the Foundation framework. If you're reading this, it's likely that you're used to the Gregorian calendar. That's the calendar that typically has 365 days in a year unless it's a leap year and 24 hours in a day unless a leap second is added. I don't want to go into how messy dates are, so I'm going to leave it at that, but let's have some fun exploring calendars!

Take a look at the following code:

var gregorianCalendar = Calendar(identifier: .gregorian)
var japaneseCalendar = Calendar(identifier: .japanese)
var hebrewCalendar = Calendar(identifier: .hebrew)

func currentDate(for calendar: Calendar) -> DateComponents {
  calendar.dateComponents([.year, .month, .day], from: Date())
}

print("Gregorian", currentDate(for: gregorianCalendar))
print("Japanese", currentDate(for: japaneseCalendar))
print("Hebrew", currentDate(for: hebrewCalendar))

How different do you expect the output for each call to currentDate(for:) to be? Will the years all be the same? What about the months? Or the day of the month? Let's look at the output of the preceding code:

Gregorian year: 2019 month: 12 day: 11 isLeapMonth: false 
Japanese year: 1 month: 12 day: 11 isLeapMonth: false 
Hebrew year: 5780 month: 3 day: 13 isLeapMonth: false 

If you guessed that the dates would all be completely different, you were right! The Calendar object can take dates, like the current date and represent them as DateComponents for whatever the Calendar represents. So we can take the current date on your machine, and convert it to a date representation that makes sense for the calendar you use to represent dates.

This also works the other way around, we can use DateComponents to represent a date that's based on a specific Calendar:

var components = DateComponents()
components.year = 1989
components.month = 11
components.day = 15

func date(for components: DateComponents, using calendar: Calendar) -> Date? {
  return calendar.date(from: components)
}

print("Gregorian", date(for: components, using: gregorianCalendar)!)
print("Japanese", date(for: components, using: japaneseCalendar)!)
print("Hebrew", date(for: components, using: hebrewCalendar)!)

The preceding code creates a DateComponents object that's based on the current calendar of the machine that's running this code. So in my case, it's Gregorian. Let's see what my birthday is on the Japanese and Hebrew calendars:

Gregorian 1989-11-14 23:00:00 +0000
Japanese 4007-11-14 23:00:00 +0000
Hebrew -1771-06-25 23:40:28 +0000

Notice how the Gregorian date is not what you would expect. The DateComponents were configured for November 15th. However, the printed date is November 14th. The reason for this is that we didn't specify a timezone for the calendar. Because of this, it defaults to GMT and since I'm not in the GMT timezone but in the UTC timezone, I get the wrong date. If you set components.timeZone to the timezone that you're representing your date in, you will get the expected output. So setting the timezone on the components objects like this: components.timeZone = TimeZone(identifier: "UTC") will produce the following output:

Gregorian 1989-11-15 00:00:00 +0000
Japanese 4007-11-15 00:00:00 +0000
Hebrew -1771-06-26 00:00:00 +0000

Much better. As you might realize by now, DateComponents describe dates using their components. In addition to year, month and day it's possible to specify things like era, minute, second, quarter, microsecond and much more. It's also possible to take a date and extract its DateComponents as I've shown in the first code snippet of this section.

Now that you have some working knowledge of Calendar and DateComponent, let's see how this applies to scheduling recurring notifications with UNCalendarNotificationTrigger.

Scheduling notifications using a UNCalendarNotificationTrigger

You probably didn't come here to spend a bunch of time reading about calendars and dates, you wanted to see how to schedule recurring notifications based on the time of day, day of the week, day of the month or pretty much any other similar kind of rule. To do that, I had to show you some examples of calendars. If you've never seen calendars and date components before, it would be incredibly hard to explain UNCalendarNotificationTrigger to you.

I'm going to assume that you have basic knowledge of asking a user for permission to send them notifications, and what the best way is to do this. If you need a quick reminder, here's how you ask for notification permissions in code:

UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { success, error in

  if error == nil && success {
    print("we have permission")
  } else {
    print("something went wrong or we don't have permission")
  }
}

Once you have the user's permission to send them notifications, you can schedule your local notifications as needed. Let's start with a simple example. Imagine sending the user a local notification every day at 09:00 am. You would use the following code to do this:

// 1
var dateComponents = DateComponents()
dateComponents.hour = 9
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)

// 2
let content = UNMutableNotificationContent()
content.title = "Daily reminder"
content.body = "Enjoy your day!"

let randomIdentifier = UUID().uuidString
let request = UNNotificationRequest(identifier: randomIdentifier, content: content, trigger: trigger)

// 3
UNUserNotificationCenter.current().add(request) { error in
  if error != nil {
    print("something went wrong")
  }
}

The preceding code snippet creates a UNCalendarNotificationTrigger using a DateComponents object where only the hour is set to 9. This means that the notification will trigger as soon as the user's current date reaches the ninth hour of the day. Keep in mind that for this to work, the calendar that the user uses and the one that's used to configure the DateComponent must match. Since DateComponent uses the current calendar it's not a problem in this case. But if you allow the user to configure their reminder, and your UI assumes a Gregorian calendar while the user's device uses a different calendar, you might run into trouble.

You can configure your date components however you want, and every time the current date on the device matches your criteria, a notification will be shown to the user. If you want to show a notification only on Wednesdays at 2:00 pm, you could use the following DateComponents configuration:

var dateComponents = DateComponents()
dateComponents.hour = 14
dateComponents.weekday = 3

Keep in mind that not all calendars start their week on the same day. Most calendars will start their week on a Monday, but some might start on a Sunday. You can account for this by grabbing the current calendar's firstWeekday property and calculating its offset from the day you're targeting. Math for dates is quite complicated so make sure to always triple check your work.

With this knowledge, you should now be able to schedule recurring notification using DateComponents!

In Summary

In this article, you learned that dates are not the same everywhere in the world. Some calendars that are used in certain places of the world are completely different from the Gregorian calendar that I am familiar with. Luckily, the Calendar and DateComponents objects from Foundation make these differences somewhat manageable because they allow us to convert from a date represented in one calendar, to the same moment in time on another calendar. You saw an example of this in the first section of this article.

Once you learned the basics of Calendar and DateComponents, you learned how to use your new knowledge to schedule recurring notifications based on DateComponents using the UNCalendarNotificationTrigger class. It's really cool to be able to schedule notifications using this trigger because it allows you to set up notifications that repeat indefinitely and fire on a certain time, day of the week, month or any other interval that you can represent using DateComponent.

If you have any questions, feedback or anything else for me, don't hesitate to reach out on Twitter. I love hearing from you!

Categories

Advent of Swift

Subscribe to my newsletter