Jul 27 - Update code for scheduled tasks 4

This commit is contained in:
Nguyen Ngo
2026-07-27 16:43:42 -04:00
parent 4132373fee
commit 9926739cb6
9 changed files with 162 additions and 49 deletions
+40
View File
@@ -10,6 +10,7 @@ import SwiftData
import SwiftUI
import Combine
import UserNotifications
import UIKit // beginBackgroundTask see beginSyncBackgroundTask()
@MainActor
class SyncManager: ObservableObject {
@@ -110,6 +111,36 @@ class SyncManager: ObservableObject {
pollTask = nil
}
// Background task assertion
// Keeps the process alive across a suspend so an in-flight sync can finish.
// Distinct from the BGProcessingTask in JanitorialQCApp: that one asks iOS
// to WAKE us later, this one asks it not to suspend us right now.
private var syncBackgroundTaskId: UIBackgroundTaskIdentifier = .invalid
private func beginSyncBackgroundTask() {
// triggerSync() is re-entrancy guarded, but assert defensively anyway:
// beginning a second assertion would leak the first identifier.
guard syncBackgroundTaskId == .invalid else { return }
syncBackgroundTaskId = UIApplication.shared.beginBackgroundTask(
withName: "JQC.syncDrain"
) {
// Called on the main thread when the grace period runs out. It MUST
// end the assertion or iOS terminates the app. assumeIsolated is
// required because the handler is a nonisolated closure under
// SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor.
MainActor.assumeIsolated {
SyncManager.shared.endSyncBackgroundTask()
}
}
}
private func endSyncBackgroundTask() {
guard syncBackgroundTaskId != .invalid else { return }
UIApplication.shared.endBackgroundTask(syncBackgroundTaskId)
syncBackgroundTaskId = .invalid
}
/// Called on logout so the next login starts a clean fetch.
func resetNotificationPoller() {
lastNotificationFetch = nil
@@ -232,6 +263,15 @@ class SyncManager: ObservableObject {
syncError = nil
defer { isSyncing = false }
// Ask iOS to keep the process alive long enough to finish the drain.
// The case this covers: an inspector taps Submit and immediately locks
// the iPad or swipes to another app. Without an assertion the process
// suspends mid-upload and the work waits for the next launch.
// Roughly 30 s of grace; the expiration handler ends it cleanly so iOS
// never force-kills us. Harmless in the foreground it simply ends.
beginSyncBackgroundTask()
defer { endSyncBackgroundTask() }
await processPhotoQueue(context: context)
await processInspectionQueue(context: context)
await processIssueQueue(context: context)