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

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

187 lines
6.6 KiB
Swift

import SwiftUI
import SwiftData
/// /
/// `reminder == nil` ;()
/// @State , SwiftData + ;
struct CustomReminderEditSheet: View {
@Environment(\.modelContext) private var ctx
@Environment(\.dismiss) private var dismiss
/// nil =
let reminder: CustomReminder?
@State private var title = ""
@State private var note = ""
@State private var pickedTime: Date = .now
@State private var weekdays: Set<Int> = Set(1...7)
@State private var hydrated = false
@State private var showAuthDeniedAlert = false
init(reminder: CustomReminder? = nil) {
self.reminder = reminder
}
private var isEditing: Bool { reminder != nil }
private var trimmedTitle: String {
title.trimmingCharacters(in: .whitespacesAndNewlines)
}
private var canSave: Bool { !trimmedTitle.isEmpty && !weekdays.isEmpty }
var body: some View {
NavigationStack {
Form {
Section {
TextField(String(appLoc: "做点什么?例:跑步5公里 / 吃2片护肝片"),
text: $title, axis: .vertical)
.lineLimit(1...3)
TextField(String(appLoc: "备注(可选)"), text: $note, axis: .vertical)
.lineLimit(1...3)
.foregroundStyle(Tj.Palette.text2)
}
Section {
DatePicker(String(appLoc: "时间"), selection: $pickedTime,
displayedComponents: .hourAndMinute)
}
Section {
weekdayRow
} header: {
Text("重复")
}
if isEditing {
Section {
Button(role: .destructive) { deleteReminder() } label: {
Label(String(appLoc: "删除提醒"), systemImage: "trash")
}
}
}
}
.scrollContentBackground(.hidden)
.background(Tj.Palette.sand.ignoresSafeArea())
.navigationTitle(isEditing ? String(appLoc: "编辑提醒") : String(appLoc: "新建提醒"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarLeading) {
Button(String(appLoc: "取消")) { dismiss() }
}
ToolbarItem(placement: .topBarTrailing) {
Button(String(appLoc: "保存")) { save() }
.fontWeight(.semibold)
.disabled(!canSave)
}
}
.onAppear(perform: hydrate)
.alert(String(appLoc: "通知未开启"), isPresented: $showAuthDeniedAlert) {
Button(String(appLoc: "")) { dismiss() }
} message: {
Text("提醒已保存,但系统通知权限未开启,到点不会弹出。请在「设置 · 通知 · 康康」中允许。")
}
}
}
// MARK: - ( RemindersListView )
private var weekdayRow: some View {
let names = [
String(appLoc: ""), String(appLoc: ""), String(appLoc: ""),
String(appLoc: ""), String(appLoc: ""), String(appLoc: ""),
String(appLoc: ""),
]
let values = [2, 3, 4, 5, 6, 7, 1]
return HStack(spacing: 6) {
ForEach(Array(values.enumerated()), id: \.offset) { idx, w in
let on = weekdays.contains(w)
Button {
if on { weekdays.remove(w) } else { weekdays.insert(w) }
} label: {
Text(names[idx])
.font(.system(size: 13, weight: on ? .semibold : .regular))
.foregroundStyle(on ? Tj.Palette.paper : Tj.Palette.text)
.frame(maxWidth: .infinity, minHeight: 30)
.background(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(on ? Tj.Palette.ink : Tj.Palette.paper)
)
.overlay(
RoundedRectangle(cornerRadius: 8, style: .continuous)
.strokeBorder(Tj.Palette.line, lineWidth: on ? 0 : 1)
)
}
.buttonStyle(.plain)
}
}
.listRowBackground(Color.clear)
}
// MARK: -
private func hydrate() {
guard !hydrated else { return }
hydrated = true
if let r = reminder {
title = r.title
note = r.note
weekdays = Set(r.weekdays)
pickedTime = Calendar.current.date(
bySettingHour: r.hour, minute: r.minute, second: 0, of: .now
) ?? .now
}
}
private func save() {
guard canSave else { return }
let cal = Calendar.current
let hour = cal.component(.hour, from: pickedTime)
let minute = cal.component(.minute, from: pickedTime)
let sortedDays = weekdays.sorted()
let target: CustomReminder
if let r = reminder {
r.title = trimmedTitle
r.note = note.trimmingCharacters(in: .whitespacesAndNewlines)
r.hour = hour
r.minute = minute
r.weekdays = sortedDays
r.updatedAt = .now
target = r
} else {
let new = CustomReminder(
title: trimmedTitle,
note: note.trimmingCharacters(in: .whitespacesAndNewlines),
hour: hour,
minute: minute,
weekdays: sortedDays
)
ctx.insert(new)
target = new
}
try? ctx.save()
Task { @MainActor in
let state = await ReminderService.requestAuthorization()
await ReminderService.sync(target)
if state == .denied {
showAuthDeniedAlert = true
} else {
dismiss()
}
}
}
private func deleteReminder() {
guard let r = reminder else { return }
ReminderService.cancel(customId: r.id)
ctx.delete(r)
try? ctx.save()
dismiss()
}
}
#Preview("新建") {
CustomReminderEditSheet()
.modelContainer(for: [CustomReminder.self], inMemory: true)
}