What does a UNCalendarNotificationTrigger actually match: time zones, daylight saving gaps and past dates, and what makes an attachment fail?
Predict first
What do these triggers resolve to?
import Foundation
import UserNotifications
let ny = TimeZone(identifier: "America/New_York")!
let iso = ISO8601DateFormatter(); iso.timeZone = ny
func next(_ dc: DateComponents, repeats: Bool = false) -> String {
UNCalendarNotificationTrigger(dateMatching: dc, repeats: repeats).nextTriggerDate().map { iso.string(from: $0) } ?? "nil"
}
let fixed = DateComponents(timeZone: ny, year: 2099, month: 7, day: 4, hour: 9, minute: 30)
print("fixed date:", next(fixed))
let gap = DateComponents(timeZone: ny, year: 2099, month: 3, day: 8, hour: 2, minute: 30)
print("nonexistent local time (spring forward):", next(gap))
let overlap = DateComponents(timeZone: ny, year: 2099, month: 11, day: 1, hour: 1, minute: 30)
print("ambiguous local time (fall back):", next(overlap))
let past = DateComponents(timeZone: ny, year: 2020, month: 1, day: 1, hour: 9)
print("non-repeating date in the past:", next(past))
let t = UNTimeIntervalNotificationTrigger(timeInterval: 30, repeats: false)
print("interval trigger:", t.timeInterval, t.repeats)
do { _ = try UNNotificationAttachment(identifier: "a", url: URL(fileURLWithPath: "/etc/hosts")) } catch { print("attachment:", (error as NSError).domain, (error as NSError).code) }
- The gap time moves to 03:30, the ambiguous time takes the first occurrence, a past date is nil, and a text file is not a valid attachment
- The gap time stays 02:30
- A past date fires immediately
Mental model. A calendar trigger is a recurring alarm clock: it knows wall-clock rules, and on the night the clocks change those rules have to pick a side.
A calendar trigger matches the components you set: hour: 9, minute: 30 with repeats: true means every day at 9:30, weekday: 2 adds Mondays, and a fully specified date with repeats: false fires once. The components carry a time zone (or default to the current one): in the run a trigger for 2099-07-04 09:30 in New York resolves to that instant with offset −04:00. Daylight saving is where schedules surprise: 02:30 on a spring-forward day does not exist and resolved to 03:30, while the ambiguous 01:30 on a fall-back day resolved to the first occurrence (−04:00). A non-repeating trigger whose date is already past has nextTriggerDate() equal to nil and simply never fires, so check it when scheduling from user input. Interval triggers (timeInterval: 30) count seconds from scheduling and must be greater than zero, and at least 60 when repeating, or the initialiser raises an exception.
Attachments (UNNotificationAttachment) validate the file type and size when created: a plain text file such as /etc/hosts fails with UNErrorDomain code 101 (invalid attachment file type), so check the error and use supported image, audio or video types. The system also copies the file, so the original may be deleted afterwards. These checks ran in a command-line tool on macOS; actually scheduling needs an app bundle and permission, and the 64-pending limit and delivery behaviour were not run here.
Triggers resolve wall-clock components in a time zone; impossible or past dates behave as shown.
Common traps
- Assuming a past date still fires. It never does; nextTriggerDate() is nil.
- Using a fixed UTC offset for a local reminder. Set a time zone or use the user’s calendar so daylight saving moves it.
They might follow up with
- How do I test a trigger? Read
nextTriggerDate()for fixed dates, as the run does.
Related questions
- 17.1.1 How do local and remote notifications work with UserNotifications: authorization, triggers, categories, and…
← How do you budget an APNs payload: bytes versus characters, escaped…