Files
psu-schedule/ios/PSUSchedule/Reminders.swift
Zakhar Timoshenko 170a44c377 Add date and time handling, models, schedule management, storage, and widget support
- Introduced `Dates.swift` for handling date and time in the university's timezone.
- Created `Models.swift` to define data structures for timetable, lessons, and directory search results.
- Implemented `Schedule.swift` for managing lesson slots and timetable functionalities.
- Developed `Storage.swift` for user settings and data persistence.
- Added `Widgets.swift` to create widgets displaying lessons and upcoming classes.
2026-10-18 20:26:12 +03:00

58 lines
3.1 KiB
Swift

import Foundation
import UserNotifications
/**
* Local reminders before each lesson of the coming week, from the saved timetable: they work
* offline, no push server is involved. iOS keeps at most 64 pending requests per app.
*/
enum Reminders {
private static let prefix = "lesson-"
private static let limit = 48
/** Asks for permission; false when the student declined. */
static func authorize() async -> Bool {
(try? await UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge])) ?? false
}
/** Replaces every scheduled reminder with the ones the timetable needs now. */
static func reschedule(timetable: Timetable?, settings: UserSettings) {
let center = UNUserNotificationCenter.current()
Task {
let pending = await center.pendingNotificationRequests().map(\.identifier).filter { $0.hasPrefix(prefix) }
center.removePendingNotificationRequests(withIdentifiers: pending)
guard settings.reminders, let t = timetable else { return }
let now = Date()
// A week ahead, so a Friday evening already holds Monday's reminders.
let horizon = now.addingTimeInterval(7 * 24 * 3600)
var count = 0
for day in t.days {
guard let date = CivilDate(day.date), count < limit else { continue }
// One reminder per start time: parallel subgroups share the notification.
for slot in t.slots(on: date, subgroup: settings.subgroup, hidden: settings.hidden) {
let begins = date.at(slot.start)
let at = begins.addingTimeInterval(-Double(settings.reminderMinutes) * 60)
guard at > now, begins < horizon, count < limit else { continue }
let content = UNMutableNotificationContent()
content.title = slot.lessons.map(\.subject).joined(separator: " / ")
let first = slot.lessons[0]
let place = Array(Set(slot.lessons.flatMap(\.rooms))).sorted().joined(separator: ", ")
var text = slot.start.description
if let pair = first.pair { text += " · \(pair) пара" }
if !place.isEmpty { text += " · ауд. \(place)" }
if slot.lessons.contains(where: \.uncertain) { text += " · сверьте недели" }
content.body = text
content.sound = .default
content.interruptionLevel = .timeSensitive
let parts = minskCalendar.dateComponents(in: minsk, from: at)
let trigger = UNCalendarNotificationTrigger(
dateMatching: DateComponents(timeZone: minsk, year: parts.year, month: parts.month, day: parts.day, hour: parts.hour, minute: parts.minute),
repeats: false
)
try? await center.add(UNNotificationRequest(identifier: prefix + slot.id, content: content, trigger: trigger))
count += 1
}
}
}
}
}