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 frequency: CustomReminder.Frequency = .daily @State private var weekdays: Set = Set(1...7) @State private var dayOfMonth = 1 @State private var month = 1 @State private var hydrated = false @State private var showAuthDeniedAlert = false /// 常用时间快捷预设(时, 分):早 / 午 / 傍晚 / 睡前。 private let timePresets: [(h: Int, m: Int)] = [(8, 0), (12, 0), (18, 0), (22, 0)] 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 { guard !trimmedTitle.isEmpty else { return false } if frequency == .weekly { return !weekdays.isEmpty } return true } 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 { Picker(String(appLoc: "重复"), selection: $frequency) { Text(String(appLoc: "每日")).tag(CustomReminder.Frequency.daily) Text(String(appLoc: "每周")).tag(CustomReminder.Frequency.weekly) Text(String(appLoc: "每月")).tag(CustomReminder.Frequency.monthly) Text(String(appLoc: "每年")).tag(CustomReminder.Frequency.yearly) } .pickerStyle(.segmented) .listRowBackground(Color.clear) frequencyDetail } header: { Text("重复") } Section { timePresetRow DatePicker(String(appLoc: "时间"), selection: $pickedTime, displayedComponents: .hourAndMinute) } 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) .onChange(of: month) { _, newMonth in // 切月份后,把超出该月最大天数的「日」收回(避免「2月31日」这种永不触发的组合)。 let maxD = Self.daysInMonth(newMonth) if dayOfMonth > maxD { dayOfMonth = maxD } } .alert(String(appLoc: "通知未开启"), isPresented: $showAuthDeniedAlert) { Button(String(appLoc: "好")) { dismiss() } } message: { Text("提醒已保存,但系统通知权限未开启,到点不会弹出。请在「设置 · 通知 · 康康」中允许。") } } } // MARK: - 频率子控件 @ViewBuilder private var frequencyDetail: some View { switch frequency { case .daily: EmptyView() case .weekly: weekdayRow case .monthly: Picker(String(appLoc: "日期"), selection: $dayOfMonth) { ForEach(1...31, id: \.self) { d in Text(String(appLoc: "\(d)日")).tag(d) } } if dayOfMonth >= 29 { skipHint } case .yearly: Picker(String(appLoc: "月份"), selection: $month) { ForEach(1...12, id: \.self) { mo in Text(String(appLoc: "\(mo)月")).tag(mo) } } Picker(String(appLoc: "日期"), selection: $dayOfMonth) { ForEach(1...Self.daysInMonth(month), id: \.self) { d in Text(String(appLoc: "\(d)日")).tag(d) } } if month == 2 && dayOfMonth == 29 { skipHint } // 仅闰年的 2/29 } } private var skipHint: some View { Text(String(appLoc: "部分月份无此日,该月将跳过")) .font(.system(size: 11)) .foregroundStyle(Tj.Palette.text3) } /// 某月最大天数(2 月取 29,允许设闰年 2/29)。 private static func daysInMonth(_ month: Int) -> Int { switch month { case 2: return 29 case 4, 6, 9, 11: return 30 default: return 31 } } // MARK: - 时间快捷预设 private var timePresetRow: some View { let cal = Calendar.current let curH = cal.component(.hour, from: pickedTime) let curM = cal.component(.minute, from: pickedTime) return HStack(spacing: 8) { ForEach(Array(timePresets.enumerated()), id: \.offset) { _, preset in let on = curH == preset.h && curM == preset.m Button { pickedTime = cal.date(bySettingHour: preset.h, minute: preset.m, second: 0, of: pickedTime) ?? pickedTime } label: { Text(String(format: "%d:%02d", preset.h, preset.m)) .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: - 周几选择(与 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 frequency = r.frequency weekdays = Set(r.weekdays) dayOfMonth = r.dayOfMonth month = r.month 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.frequency = frequency r.dayOfMonth = dayOfMonth r.month = month r.updatedAt = .now target = r } else { let new = CustomReminder( title: trimmedTitle, note: note.trimmingCharacters(in: .whitespacesAndNewlines), hour: hour, minute: minute, weekdays: sortedDays, frequency: frequency, dayOfMonth: dayOfMonth, month: month ) 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) }