意外とローカル通知周りは公式ドキュメントが少ないということもあり、変なところで情報不足によりハマりやすかったりします。(パッと見た感じScheduling a Notification Locally from Your Appの1本のみ)
UNCalendarNotificationTriggerはdateComponentsを渡してカレンダーによるローカル通知を計画するクラスですが、パラメーターの渡し方によっては上手く通知が発火してくれないことがあります。この記事では上手くいくパターン、上手く行かないパターンを1つづつお見せします。
題材
UIKitを使って作成した初期プロジェクトに、UIDatePickerとUIButtonだけを貼り付けた画面を使います。
上手く行かない例
こちらは失敗する例です。後で解説する上手くいく例も含めて、UNUserNotificationCenter.current(). requestAuthorizationを利用して認可を得たあとにクロージャで通知の設定をしています。
こちらでは、Calendar.current.dateComponents(in:, from:)を利用してdateComponentsを得ています。
import UIKit
import UserNotifications
class ViewController: UIViewController {
@IBOutlet weak var calendar: UIDatePicker!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
@IBAction func scheduleNotification(_ sender: Any) {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { [unowned self] authorized, error in
if let error = error {
print("error: \(error)")
return
}
print("authorized: \(authorized)")
UNUserNotificationCenter.current().delegate = self
DispatchQueue.main.async {
let notificationContent = UNMutableNotificationContent()
notificationContent.title = "Test"
notificationContent.body = "Test Body"
notificationContent.sound = .default
notificationContent.badge = 1
let dateComponents = Calendar.current.dateComponents(in: TimeZone.current, from: self.calendar.date)
print("datecomponents: \(dateComponents)")
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: false)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("error: \(error)")
} else {
print("schedule successeded!")
}
}
}
}
}
}
extension ViewController: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.badge, .banner, .list, .sound])
}
}
ログを見ていくと、出力は以下のようになっています
authorized: true
datecomponents: calendar: gregorian (current) timeZone: Asia/Tokyo (current) era: 1 year: 2021 month: 9 day: 6 hour: 8 minute: 53 second: 0 nanosecond: 0 weekday: 2 weekdayOrdinal: 1 quarter: 0 weekOfMonth: 2 weekOfYear: 37 yearForWeekOfYear: 2021 isLeapMonth: false
schedule successeded!
年月日と時間までは意図的に通知に利用したいですが、weekday: 2 weekdayOrdinal: 1 quarter: 0 weekOfMonth: 2 weekOfYear: 37 yearForWeekOfYear: 2021
という意図しない不要なdateComponentまで返されてしまっています。
この通知は期待通りの時間に届きません。
上手くいく例
こちらの実装では、dateComponentsをCalendar.dateComponents(in:from:)を利用して取得するのを辞め、必要なものだけ取得して渡すようにしています。
import UIKit
import UserNotifications
class ViewController: UIViewController {
@IBOutlet weak var calendar: UIDatePicker!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
}
@IBAction func scheduleNotification(_ sender: Any) {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound]) { [unowned self] authorized, error in
if let error = error {
print("error: \(error)")
return
}
print("authorized: \(authorized)")
UNUserNotificationCenter.current().delegate = self
DispatchQueue.main.async {
let notificationContent = UNMutableNotificationContent()
notificationContent.title = "Test"
notificationContent.body = "Test Body"
notificationContent.sound = .default
notificationContent.badge = 1
var dateComponents = DateComponents()
dateComponents.year = Calendar.current.component(.year, from: self.calendar.date)
dateComponents.month = Calendar.current.component(.month, from: self.calendar.date)
dateComponents.day = Calendar.current.component(.day, from: self.calendar.date)
// 時間を利用した通知だと上手くいく
dateComponents.hour = Calendar.current.component(.hour, from: self.calendar.date)
dateComponents.minute = Calendar.current.component(.minute, from: self.calendar.date)
dateComponents.calendar = Calendar.current
dateComponents.timeZone = TimeZone.current
print("datecomponents: \(dateComponents)")
let trigger = UNCalendarNotificationTrigger(dateMatching: dateComponents, repeats: false)
let request = UNNotificationRequest(identifier: UUID().uuidString, content: notificationContent, trigger: trigger)
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
print("error: \(error)")
} else {
print("schedule successeded!")
}
}
}
}
}
}
extension ViewController: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.badge, .banner, .list, .sound])
}
}
ログを見ていくと、出力は以下のようになっています
authorized: true
datecomponents: calendar: gregorian (current) timeZone: Asia/Tokyo (current) year: 2021 month: 9 day: 6 hour: 8 minute: 36 isLeapMonth: false
schedule successeded!
先程の上手く行かなかった例とは違い、不必要なdateComponentが設定されていないことが分かります。
この通知は期待通りの時間に届きます。
問題の原因
この挙動に関して詳しい情報は公式ドキュメントにもほぼないのですが、一点だけ以下のような記載があります。
dateComponents
The temporal information to use when constructing the trigger. Provide only the date components that are relevant for your trigger.
(Apple inc., Parameters, init(dateMatching:repeats:), https://developer.apple.com/documentation/usernotifications/uncalendarnotificationtrigger/1649772-init, viewed: 2021/09/06)
「トリガーに関係あるdateComponentsだけ提供してくれ」と書いてあるため、トリガーに関係のないcomponentまで返してしまうdateComponents(in:from:)を利用したことで通知が期待通りに発火しない結果を引き起こしてしまったと考えられます。
解決策
上手くいく案でお見せしたように、新規にDateComponentsのインスタンスを作成して必要なものだけ設定していくか、指定したcomponentだけ返してくれるdateComponents(_:from:)などを利用して設定を行なっていくことで、正しく日付をトリガーとした通知を受け取ることができます。
おすすめの方法はdateComponents(_:from:)を利用し、以下のように取得するものです。
let dateComponents = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: self.calendar.date)
まとめ
- この記事ではUNCalendarNotificationTriggerの初期化時に、渡すdateComponentsのパラメーターによっては期待通りに通知が実行されない問題があることをお見せしました。
- 合わせて上手くいく例をお見せし、問題の原因について考えました。
- 意図通りに日付によるローカル通知を実行させるための解決策をお伝えしました。