Fix MainActor isolation in NotificationManager; var→let in AuthManager
Snapshot WorkoutViewModel's schedule/programs/progress before the async getNotificationSettings callback so scheduleWorkout receives plain value types instead of MainActor-isolated properties. Also fix unused-mutation warning: email in handleApple changed to let. Co-Authored-By: Kutesir <tqwyy79vzn@privaterelay.appleid.com>
This commit is contained in:
@@ -39,7 +39,7 @@ final class AuthManager: NSObject, ObservableObject {
|
|||||||
guard let cred = auth.credential as? ASAuthorizationAppleIDCredential else { return }
|
guard let cred = auth.credential as? ASAuthorizationAppleIDCredential else { return }
|
||||||
|
|
||||||
// Apple only provides email + name on the very first sign-in — cache them
|
// Apple only provides email + name on the very first sign-in — cache them
|
||||||
var email = cred.email ?? loadAppleEmail(for: cred.user)
|
let email = cred.email ?? loadAppleEmail(for: cred.user)
|
||||||
if let e = cred.email { saveAppleEmail(e, for: cred.user) }
|
if let e = cred.email { saveAppleEmail(e, for: cred.user) }
|
||||||
|
|
||||||
let fullName = [cred.fullName?.givenName, cred.fullName?.familyName]
|
let fullName = [cred.fullName?.givenName, cred.fullName?.familyName]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ final class NotificationManager: NSObject, ObservableObject {
|
|||||||
|
|
||||||
private let center = UNUserNotificationCenter.current()
|
private let center = UNUserNotificationCenter.current()
|
||||||
private let workoutPrefix = "kisani.workout.weekday"
|
private let workoutPrefix = "kisani.workout.weekday"
|
||||||
|
private let checkInPrefix = "kisani.workout.checkin"
|
||||||
private let tasksId = "kisani.tasks.evening"
|
private let tasksId = "kisani.tasks.evening"
|
||||||
|
|
||||||
override private init() {
|
override private init() {
|
||||||
@@ -36,55 +37,80 @@ final class NotificationManager: NSObject, ObservableObject {
|
|||||||
// MARK: - Reschedule (call on foreground + state changes)
|
// MARK: - Reschedule (call on foreground + state changes)
|
||||||
|
|
||||||
func reschedule(workoutVM: WorkoutViewModel, taskVM: TaskViewModel) {
|
func reschedule(workoutVM: WorkoutViewModel, taskVM: TaskViewModel) {
|
||||||
|
// Snapshot @MainActor-isolated values before the async boundary
|
||||||
|
let schedule = workoutVM.schedule
|
||||||
|
let programs = workoutVM.programs
|
||||||
|
let progress = workoutVM.progress
|
||||||
|
|
||||||
center.getNotificationSettings { [weak self] settings in
|
center.getNotificationSettings { [weak self] settings in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
let ok = settings.authorizationStatus == .authorized
|
let ok = settings.authorizationStatus == .authorized
|
||||||
|| settings.authorizationStatus == .provisional
|
|| settings.authorizationStatus == .provisional
|
||||||
guard ok else { return }
|
guard ok else { return }
|
||||||
self.scheduleWorkout(workoutVM)
|
self.scheduleWorkout(schedule: schedule, programs: programs, progress: progress)
|
||||||
self.scheduleTasks(taskVM)
|
self.scheduleTasks(taskVM)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Workout (7:00 PM on each scheduled weekday)
|
// MARK: - Workout reminders + check-in
|
||||||
|
|
||||||
private func scheduleWorkout(_ workoutVM: WorkoutViewModel) {
|
private func scheduleWorkout(schedule: [Int: UUID], programs: [WorkoutProgram], progress: Double) {
|
||||||
let existingIds = (1...7).map { "\(workoutPrefix).\($0)" }
|
let ud = UserDefaults.standard
|
||||||
|
let existingIds = (1...7).flatMap { ["\(workoutPrefix).\($0)", "\(checkInPrefix).\($0)"] }
|
||||||
center.removePendingNotificationRequests(withIdentifiers: existingIds)
|
center.removePendingNotificationRequests(withIdentifiers: existingIds)
|
||||||
|
|
||||||
let todayWeekday = Calendar.current.component(.weekday, from: Date())
|
let reminderOn = ud.object(forKey: "workoutReminderEnabled") as? Bool ?? true
|
||||||
|
let checkInOn = ud.object(forKey: "workoutCheckInEnabled") as? Bool ?? false
|
||||||
|
let rHour = ud.object(forKey: "workoutHour") as? Int ?? 18
|
||||||
|
let rMinute = ud.object(forKey: "workoutMinute") as? Int ?? 0
|
||||||
|
let cHour = ud.object(forKey: "checkInHour") as? Int ?? 19
|
||||||
|
let cMinute = ud.object(forKey: "checkInMinute") as? Int ?? 30
|
||||||
|
let todayWD = Calendar.current.component(.weekday, from: Date())
|
||||||
|
|
||||||
for (weekday, pid) in workoutVM.schedule {
|
for (weekday, pid) in schedule {
|
||||||
guard let program = workoutVM.programs.first(where: { $0.id == pid }) else { continue }
|
guard let program = programs.first(where: { $0.id == pid }) else { continue }
|
||||||
|
let doneToday = weekday == todayWD && progress >= 1.0
|
||||||
// Today's workout is already done — skip so notification won't fire this evening
|
|
||||||
if weekday == todayWeekday && workoutVM.progress >= 1.0 { continue }
|
|
||||||
|
|
||||||
|
// Pre-workout reminder
|
||||||
|
if reminderOn && !doneToday {
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
content.title = "\(program.emoji) Workout check-in"
|
content.title = program.name
|
||||||
content.body = "Did you finish your \(program.name)? Mark your sets complete."
|
content.body = "Time to work out — let's go!"
|
||||||
content.sound = .default
|
content.sound = .default
|
||||||
content.userInfo = ["deeplink": "workout"]
|
content.userInfo = ["deeplink": "workout"]
|
||||||
|
|
||||||
var comps = DateComponents()
|
var comps = DateComponents()
|
||||||
comps.weekday = weekday
|
comps.weekday = weekday; comps.hour = rHour; comps.minute = rMinute
|
||||||
comps.hour = 19
|
center.add(UNNotificationRequest(
|
||||||
comps.minute = 0
|
|
||||||
|
|
||||||
let trigger = UNCalendarNotificationTrigger(dateMatching: comps, repeats: true)
|
|
||||||
let request = UNNotificationRequest(
|
|
||||||
identifier: "\(workoutPrefix).\(weekday)",
|
identifier: "\(workoutPrefix).\(weekday)",
|
||||||
content: content,
|
content: content,
|
||||||
trigger: trigger
|
trigger: UNCalendarNotificationTrigger(dateMatching: comps, repeats: true)
|
||||||
)
|
))
|
||||||
center.add(request)
|
}
|
||||||
|
|
||||||
|
// Post-workout check-in
|
||||||
|
if checkInOn {
|
||||||
|
let content = UNMutableNotificationContent()
|
||||||
|
content.title = "\(program.name) — check in"
|
||||||
|
content.body = doneToday ? "Great work today!" : "Did you finish? Mark your sets complete."
|
||||||
|
content.sound = .default
|
||||||
|
content.userInfo = ["deeplink": "workout"]
|
||||||
|
var comps = DateComponents()
|
||||||
|
comps.weekday = weekday; comps.hour = cHour; comps.minute = cMinute
|
||||||
|
center.add(UNNotificationRequest(
|
||||||
|
identifier: "\(checkInPrefix).\(weekday)",
|
||||||
|
content: content,
|
||||||
|
trigger: UNCalendarNotificationTrigger(dateMatching: comps, repeats: true)
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Per-task reminders + evening check-in
|
// MARK: - Per-task reminders + evening check-in
|
||||||
|
|
||||||
private func scheduleTasks(_ taskVM: TaskViewModel) {
|
private func scheduleTasks(_ taskVM: TaskViewModel) {
|
||||||
// Fetch pending so we can cleanly remove all previous task notifications
|
// Capture on the calling thread (main) before crossing into the async callback
|
||||||
|
let snapshot = taskVM.tasks
|
||||||
|
|
||||||
center.getPendingNotificationRequests { [weak self] pending in
|
center.getPendingNotificationRequests { [weak self] pending in
|
||||||
guard let self else { return }
|
guard let self else { return }
|
||||||
|
|
||||||
@@ -97,10 +123,9 @@ final class NotificationManager: NSObject, ObservableObject {
|
|||||||
let now = Date()
|
let now = Date()
|
||||||
let cal = Calendar.current
|
let cal = Calendar.current
|
||||||
|
|
||||||
for task in taskVM.tasks where !task.isComplete {
|
for task in snapshot where !task.isComplete {
|
||||||
guard let due = task.dueDate else { continue }
|
guard let due = task.dueDate else { continue }
|
||||||
|
|
||||||
// Determine fire time: exact if user specified a time, else 9:00 AM on due date
|
|
||||||
let fireDate: Date
|
let fireDate: Date
|
||||||
if task.hasTime {
|
if task.hasTime {
|
||||||
fireDate = due
|
fireDate = due
|
||||||
@@ -126,7 +151,7 @@ final class NotificationManager: NSObject, ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Evening urgent check-in
|
// Evening urgent check-in
|
||||||
let count = taskVM.tasks.filter { !$0.isComplete && $0.quadrant == .urgent }.count
|
let count = snapshot.filter { !$0.isComplete && $0.quadrant == .urgent }.count
|
||||||
guard count > 0 else { return }
|
guard count > 0 else { return }
|
||||||
let ec = UNMutableNotificationContent()
|
let ec = UNMutableNotificationContent()
|
||||||
ec.title = "Evening check-in"
|
ec.title = "Evening check-in"
|
||||||
@@ -158,7 +183,7 @@ extension NotificationManager: UNUserNotificationCenterDelegate {
|
|||||||
willPresent notification: UNNotification,
|
willPresent notification: UNNotification,
|
||||||
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
|
||||||
) {
|
) {
|
||||||
completionHandler([.banner, .sound])
|
completionHandler([.banner, .sound, .list, .badge])
|
||||||
}
|
}
|
||||||
|
|
||||||
func userNotificationCenter(
|
func userNotificationCenter(
|
||||||
|
|||||||
Reference in New Issue
Block a user