Schedule Notifications on Day of Month
Today I wanted to implement a notification service in my new App that schedules notifications monthly for any given day.
I thought it would be an easy task because I know there is this iOS API: UNCalendarNotificationTrigger
.
With this API, I would schedule a repeat notification matching a given DateComponent
like this:
var dateComponents = DateComponents()
dateComponents.day = 2
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: true)
This all works great until I realize the ending day for each month can be different.
UNCalendarNotificationTrigger
will try to get the approximate date using a MatchingPolicy
called nextTimePreservingSmallerComponents
.
For example, since the last day of February is 28th, if you try to get February 31st, the result of calling nextTriggerDate()
of the trigger will return March 1st.
Because I want to notify the user at the end of each month, I have to schedule notifications myself instead of using UNCalendarNotificationTrigger
.
Here is what I came up with:
struct AccountNotificationScheduler {
private let calendar = Calendar.current
private let notificationHour = 10 // 10:00am
private let scheduleMonthCount = 12 // Schedule for one year
private func schedule(for month: Int, dayOfMonth: Int, title: String, body: String, accountUUID: String) {
var date = Date()
for index in 0..<month {
date = nextDate(for: dayOfMonth,
startDate: date)
var components = calendar.dateComponents(in: calendar.timeZone, from: date)
// There is a bug that quarter must set to nil (default is 0), otherwise the trigger will not fire any event
// https://stackoverflow.com/questions/41526377/swift-user-notifications-why-is-nexttriggerdate-nil
components.quarter = nil
notificationService.notify(matching: components,
title: title,
body: body,
repeats: false,
identifier: "\(index)-"+accountUUID)
}
}
// Next date for dayIndex of a month
// startDate: the date to begin the search
private func nextDate(for dayOfMonth: Int,
startDate: Date) -> Date {
var components = DateComponents()
components.day = dayOfMonth
components.hour = notificationHour
components.minute = 0
components.second = 0
components.timeZone = calendar.timeZone
return calendar.nextDate(after: todayNotificationTime, matching: components, matchingPolicy: .previousTimePreservingSmallerComponents)!
}
}
In the above example, I use a Calendar api nextDate
to search for the approximate ending date for each month, then schedule notification individually.
Unlike UNCalendarNotificationTrigger
, this API offers a matchingPolicy
that I can set to .previousTimePreservingSmallerComponents
.
This means it will always look for the nearest possible day while preserving smaller components (hours, minutes, seconds etc.).