Files
kangkang/康康/Services/ReminderService.swift
link2026 dad9d43486 ```
feat: 添加自定义提醒功能并优化项目配置

- 添加 CustomReminder 模型支持自由文案周期性提醒功能
- 实现自定义提醒的 UI 界面,包括新建、编辑和列表展示
- 集成本地通知服务支持自定义提醒的时间触发
- 更新项目配置文件添加应用显示名称和加密声明
- 修正 iOS 部署目标版本从 26.0 到 17.0
- 修复 FileDownloader 中的线程安全问题
- 优化 ModelManifest 和 Localization 的并发安全性
- 扩展本地化字符串支持多语言提醒相关文本
- 调整项目支持平台范围仅保留 iphoneos 和 iphonesimulator
```
2026-05-30 11:36:29 +08:00

140 lines
5.6 KiB
Swift

import Foundation
import UserNotifications
///
/// `metricId` iOS N weekly-repeats ,id
/// `kangkang.reminder.<metricId>.w<weekday>`,便 weekday cancel
///
/// SwiftData `MetricReminder`;,
/// SwiftData
enum ReminderService {
static let idPrefix = "kangkang.reminder."
static let customIdPrefix = "kangkang.custom."
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 else { return }
await schedule(
idBase: "\(idPrefix)\(reminder.metricId)",
title: String(appLoc: "该测\(reminder.displayName)"),
body: String(appLoc: "在「+ 新建 → 指标记录 → \(reminder.displayName)」记录一次"),
hour: reminder.hour,
minute: reminder.minute,
weekdays: reminder.weekdays,
thread: "kangkang.reminder.\(reminder.metricId)"
)
}
/// metric pending (7 weekday ,)
static func cancel(metricId: String) {
cancelBase("\(idPrefix)\(metricId)")
}
// MARK: - (CustomReminder)
/// `CustomReminder` save
static func sync(_ reminder: CustomReminder) async {
cancel(customId: reminder.id)
guard reminder.enabled else { return }
let title = reminder.title.trimmingCharacters(in: .whitespacesAndNewlines)
let body = reminder.note.trimmingCharacters(in: .whitespacesAndNewlines)
await schedule(
idBase: "\(customIdPrefix)\(reminder.id.uuidString)",
title: title.isEmpty ? String(appLoc: "提醒") : title,
body: body.isEmpty ? String(appLoc: "到点啦,记得完成") : body,
hour: reminder.hour,
minute: reminder.minute,
weekdays: reminder.weekdays,
thread: "\(customIdPrefix)\(reminder.id.uuidString)"
)
}
/// pending
static func cancel(customId: UUID) {
cancelBase("\(customIdPrefix)\(customId.uuidString)")
}
/// Me Tab
static func cancelAll() {
UNUserNotificationCenter.current().removeAllPendingNotificationRequests()
}
// MARK: -
/// weekdays N weekly-repeats
/// `idBase` `.w<weekday>` ;
private static func schedule(idBase: String,
title: String,
body: String,
hour: Int,
minute: Int,
weekdays: [Int],
thread: String) async {
guard !weekdays.isEmpty else { return }
let center = UNUserNotificationCenter.current()
let content = UNMutableNotificationContent()
content.title = title
content.body = body
content.sound = .default
content.threadIdentifier = thread
for weekday in weekdays {
var comps = DateComponents()
comps.hour = hour
comps.minute = minute
comps.weekday = weekday
let trigger = UNCalendarNotificationTrigger(dateMatching: comps, repeats: true)
let request = UNNotificationRequest(identifier: "\(idBase).w\(weekday)",
content: content,
trigger: trigger)
try? await center.add(request)
}
}
/// idBase 7 weekday pending ()
private static func cancelBase(_ idBase: String) {
let center = UNUserNotificationCenter.current()
let ids = (1...7).map { "\(idBase).w\($0)" }
center.removePendingNotificationRequests(withIdentifiers: ids)
}
}