WatchNotificationHandler.swift 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667
  1. import Foundation
  2. import UserNotifications
  3. import WatchConnectivity
  4. final class WatchNotificationHandler: NSObject, UNUserNotificationCenterDelegate {
  5. static let shared = WatchNotificationHandler()
  6. override private init() {
  7. super.init()
  8. }
  9. func configure() {
  10. let center = UNUserNotificationCenter.current()
  11. center.delegate = self
  12. registerCategories(on: center)
  13. }
  14. private func registerCategories(on center: UNUserNotificationCenter) {
  15. center.getNotificationCategories { existingCategories in
  16. let glucoseCategory = NotificationCategoryFactory.createGlucoseCategory()
  17. var categories = existingCategories
  18. categories.update(with: glucoseCategory)
  19. // UNUserNotificationCenter methods should be called on main thread
  20. Task { @MainActor in
  21. center.setNotificationCategories(categories)
  22. }
  23. }
  24. }
  25. /// UNUserNotificationCenterDelegate method called when user interacts with a notification on watch.
  26. /// This can be called off the main thread. WCSession.transferUserInfo is thread-safe.
  27. func userNotificationCenter(
  28. _: UNUserNotificationCenter,
  29. didReceive response: UNNotificationResponse,
  30. withCompletionHandler completionHandler: @escaping () -> Void
  31. ) {
  32. defer { completionHandler() }
  33. guard let action = NotificationResponseAction(rawValue: response.actionIdentifier) else { return }
  34. sendSnoozeRequest(for: action)
  35. }
  36. /// Sends snooze request to iPhone via WatchConnectivity.
  37. /// WCSession.transferUserInfo is thread-safe and can be called from any thread.
  38. /// Relies on the watch app's WCSession owner (e.g., WatchState) to handle
  39. /// session activation and delegate management.
  40. private func sendSnoozeRequest(for action: NotificationResponseAction) {
  41. guard WCSession.isSupported() else { return }
  42. let payload: [String: Any] = [WatchMessageKeys.snoozeDuration: action.minutes]
  43. let session = WCSession.default
  44. // Try sendMessage first if session is reachable and activated (faster, immediate delivery)
  45. // Fall back to transferUserInfo if not reachable or if sendMessage fails
  46. if session.isReachable, session.activationState == .activated {
  47. session.sendMessage(payload, replyHandler: nil) { _ in
  48. // Fallback to transferUserInfo if sendMessage fails
  49. session.transferUserInfo(payload)
  50. }
  51. } else {
  52. // Session not reachable or not activated - use transferUserInfo (queued delivery)
  53. session.transferUserInfo(payload)
  54. }
  55. }
  56. }