Initial commit: KisaniCal iOS app
Task management, Eisenhower matrix, workout logging with persistence, and calendar with 6 view modes (Month, List, Year, Week, 3 Day, Day). Custom floating tab bar, dynamic calendar icon, and workout data seeded.
This commit is contained in:
127
KisaniCal/Managers/NotificationManager.swift
Normal file
127
KisaniCal/Managers/NotificationManager.swift
Normal file
@@ -0,0 +1,127 @@
|
||||
import SwiftUI
|
||||
import UserNotifications
|
||||
|
||||
final class NotificationManager: NSObject, ObservableObject {
|
||||
static let shared = NotificationManager()
|
||||
|
||||
@Published var isAuthorized = false
|
||||
|
||||
private let center = UNUserNotificationCenter.current()
|
||||
private let workoutPrefix = "kisani.workout.weekday"
|
||||
private let tasksId = "kisani.tasks.evening"
|
||||
|
||||
override private init() {
|
||||
super.init()
|
||||
center.delegate = self
|
||||
refreshAuthStatus()
|
||||
}
|
||||
|
||||
// MARK: - Permission
|
||||
|
||||
func requestPermission() {
|
||||
center.requestAuthorization(options: [.alert, .sound, .badge]) { [weak self] granted, _ in
|
||||
DispatchQueue.main.async { self?.isAuthorized = granted }
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshAuthStatus() {
|
||||
center.getNotificationSettings { [weak self] settings in
|
||||
DispatchQueue.main.async {
|
||||
self?.isAuthorized = (settings.authorizationStatus == .authorized
|
||||
|| settings.authorizationStatus == .provisional)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Reschedule (call on foreground + state changes)
|
||||
|
||||
func reschedule(workoutVM: WorkoutViewModel, taskVM: TaskViewModel) {
|
||||
center.getNotificationSettings { [weak self] settings in
|
||||
guard let self else { return }
|
||||
let ok = settings.authorizationStatus == .authorized
|
||||
|| settings.authorizationStatus == .provisional
|
||||
guard ok else { return }
|
||||
self.scheduleWorkout(workoutVM)
|
||||
self.scheduleTasks(taskVM)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Workout (7:00 PM on each scheduled weekday)
|
||||
|
||||
private func scheduleWorkout(_ workoutVM: WorkoutViewModel) {
|
||||
let existingIds = (1...7).map { "\(workoutPrefix).\($0)" }
|
||||
center.removePendingNotificationRequests(withIdentifiers: existingIds)
|
||||
|
||||
let todayWeekday = Calendar.current.component(.weekday, from: Date())
|
||||
|
||||
for (weekday, pid) in workoutVM.schedule {
|
||||
guard let program = workoutVM.programs.first(where: { $0.id == pid }) else { continue }
|
||||
|
||||
// Today's workout is already done — skip so notification won't fire this evening
|
||||
if weekday == todayWeekday && workoutVM.progress >= 1.0 { continue }
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "\(program.emoji) Workout check-in"
|
||||
content.body = "Did you finish your \(program.name)? Mark your sets complete."
|
||||
content.sound = .default
|
||||
content.userInfo = ["deeplink": "workout"]
|
||||
|
||||
var comps = DateComponents()
|
||||
comps.weekday = weekday
|
||||
comps.hour = 19
|
||||
comps.minute = 0
|
||||
|
||||
let trigger = UNCalendarNotificationTrigger(dateMatching: comps, repeats: true)
|
||||
let request = UNNotificationRequest(
|
||||
identifier: "\(workoutPrefix).\(weekday)",
|
||||
content: content,
|
||||
trigger: trigger
|
||||
)
|
||||
center.add(request)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Urgent tasks (7:30 PM daily, only when there are open urgent tasks)
|
||||
|
||||
private func scheduleTasks(_ taskVM: TaskViewModel) {
|
||||
center.removePendingNotificationRequests(withIdentifiers: [tasksId])
|
||||
|
||||
let count = taskVM.tasks.filter { !$0.isComplete && $0.quadrant == .urgent }.count
|
||||
guard count > 0 else { return }
|
||||
|
||||
let content = UNMutableNotificationContent()
|
||||
content.title = "Evening task check-in"
|
||||
content.body = "\(count) urgent task\(count == 1 ? "" : "s") still open — mark done or reschedule."
|
||||
content.sound = .default
|
||||
content.userInfo = ["deeplink": "tasks"]
|
||||
|
||||
var comps = DateComponents()
|
||||
comps.hour = 19
|
||||
comps.minute = 30
|
||||
|
||||
let trigger = UNCalendarNotificationTrigger(dateMatching: comps, repeats: true)
|
||||
let request = UNNotificationRequest(identifier: tasksId, content: content, trigger: trigger)
|
||||
center.add(request)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - UNUserNotificationCenterDelegate
|
||||
|
||||
extension NotificationManager: UNUserNotificationCenterDelegate {
|
||||
// Show banners even when app is foregrounded
|
||||
func userNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
willPresent notification: UNNotification,
|
||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
||||
) {
|
||||
completionHandler([.banner, .sound])
|
||||
}
|
||||
|
||||
func userNotificationCenter(
|
||||
_ center: UNUserNotificationCenter,
|
||||
didReceive response: UNNotificationResponse,
|
||||
withCompletionHandler completionHandler: @escaping () -> Void
|
||||
) {
|
||||
completionHandler()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user