import Foundation import UserNotifications /// 周期性指标提醒的本地通知调度。 /// 同一 `metricId` 在 iOS 通知中心展开成 N 条 weekly-repeats 通知,id 形如 /// `kangkang.reminder..w`,方便按 weekday 单独 cancel。 /// /// 数据存 SwiftData `MetricReminder`;本服务只负责系统通知中心的同步, /// 不写 SwiftData。两边写入的协调由调用方负责。 enum ReminderService { static let idPrefix = "kangkang.reminder." enum AuthState: String { case granted, denied, notDetermined, provisional } // MARK: - authorization static func currentAuthState() async -> AuthState { let settings = await UNUserNotificationCenter.current().notificationSettings() switch settings.authorizationStatus { case .authorized: return .granted case .denied: return .denied case .provisional: return .provisional case .ephemeral: return .granted case .notDetermined: return .notDetermined @unknown default: return .notDetermined } } /// 申请通知权限。已 granted/denied 时直接返回当前状态。 @discardableResult static func requestAuthorization() async -> AuthState { let center = UNUserNotificationCenter.current() let settings = await center.notificationSettings() if settings.authorizationStatus != .notDetermined { return await currentAuthState() } do { let granted = try await center.requestAuthorization(options: [.alert, .sound, .badge]) return granted ? .granted : .denied } catch { return .denied } } // MARK: - upsert / cancel /// 取消该 metric 在通知中心所有 pending 通知,再按当前 enabled/时间/weekdays 重排。 /// 调用方在 `MetricReminder` save 之后调用。 static func sync(_ reminder: MetricReminder) async { cancel(metricId: reminder.metricId) guard reminder.enabled, !reminder.weekdays.isEmpty else { return } let center = UNUserNotificationCenter.current() let content = UNMutableNotificationContent() content.title = "该测\(reminder.displayName)了" content.body = "在「+ 新建 → 指标记录 → \(reminder.displayName)」记录一次" content.sound = .default content.threadIdentifier = "kangkang.reminder.\(reminder.metricId)" for weekday in reminder.weekdays { var comps = DateComponents() comps.hour = reminder.hour comps.minute = reminder.minute comps.weekday = weekday let trigger = UNCalendarNotificationTrigger(dateMatching: comps, repeats: true) let id = identifier(metricId: reminder.metricId, weekday: weekday) let request = UNNotificationRequest(identifier: id, content: content, trigger: trigger) try? await center.add(request) } } /// 取消某个 metric 的所有 pending 通知(7 个 weekday 一并取消,不漏)。 static func cancel(metricId: String) { let center = UNUserNotificationCenter.current() let ids = (1...7).map { identifier(metricId: metricId, weekday: $0) } center.removePendingNotificationRequests(withIdentifiers: ids) } /// 全清。Me Tab 一键关闭所有提醒时用。 static func cancelAll() { UNUserNotificationCenter.current().removeAllPendingNotificationRequests() } // MARK: - helpers private static func identifier(metricId: String, weekday: Int) -> String { "\(idPrefix)\(metricId).w\(weekday)" } }