【Tips】UNCalendarNotificationTrigger利用時には必要なcomponentだけ渡す

意外とローカル通知周りは公式ドキュメントが少ないということもあり、変なところで情報不足によりハマりやすかったりします。(パッと見た感じ[Scheduling a Notification Locally from Your App](https://developer.apple.com/documentation/usernotifications/scheduling_a_notification_locally_from_your_app)の1本のみ)

[UNCalendarNotificationTrigger](https://developer.apple.com/documentation/usernotifications/uncalendarnotificationtrigger)はdateComponentsを渡してカレンダーによるローカル通知を計画するクラスですが、パラメーターの渡し方によっては上手く通知が発火してくれないことがあります。この記事では上手くいくパターン、上手く行かないパターンを1つづつお見せします。

# 題材
UIKitを使って作成した初期プロジェクトに、[UIDatePicker](https://developer.apple.com/documentation/uikit/uidatepicker)と[UIButton](https://developer.apple.com/documentation/uikit/uibutton)だけを貼り付けた画面を使います。
![スクリーンショット 2021-09-06 8.44.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/114709/f71e5d53-c404-528b-edda-a572f734f712.png)

# 上手く行かない例

こちらは失敗する例です。後で解説する上手くいく例も含めて、[UNUserNotificationCenter.current(). requestAuthorization](https://developer.apple.com/documentation/usernotifications/unusernotificationcenter/1649527-requestauthorization)を利用して認可を得たあとにクロージャで通知の設定をしています。

こちらでは、[Calendar.current.dateComponents(in:, from:)](https://developer.apple.com/documentation/foundation/calendar/2293477-datecomponents)を利用してdateComponentsを得ています。

```swift
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])
    }

}
```

ログを見ていくと、出力は以下のようになっています

```console
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:)を利用して取得するのを辞め、必要なものだけ取得して渡すようにしています。

```swift
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])
    }

}

```

ログを見ていくと、出力は以下のようになっています

```console
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](https://developer.apple.com/documentation/foundation/datecomponents)のインスタンスを作成して必要なものだけ設定していくか、指定したcomponentだけ返してくれる[dateComponents(_:from:)](https://developer.apple.com/documentation/foundation/calendar/2293646-datecomponents)などを利用して設定を行なっていくことで、正しく日付をトリガーとした通知を受け取ることができます。

おすすめの方法は[dateComponents(_:from:)](https://developer.apple.com/documentation/foundation/calendar/2293646-datecomponents)を利用し、以下のように取得するものです。

```swift
let dateComponents = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: self.calendar.date)
```

# まとめ
- この記事ではUNCalendarNotificationTriggerの初期化時に、渡すdateComponentsのパラメーターによっては期待通りに通知が実行されない問題があることをお見せしました。
- 合わせて上手くいく例をお見せし、問題の原因について考えました。
- 意図通りに日付によるローカル通知を実行させるための解決策をお伝えしました。