133 lines
4.8 KiB
Swift
133 lines
4.8 KiB
Swift
// JQCApp.swift
|
|
// ------------
|
|
// App entry point.
|
|
|
|
import SwiftUI
|
|
import SwiftData
|
|
import BackgroundTasks
|
|
import UserNotifications
|
|
|
|
// ── AppDelegate — runtime orientation lock ────────────────────────────────────
|
|
// Info.plist must declare all 4 orientations so iPad multitasking is supported
|
|
// (App Store requirement). This delegate restricts the app to landscape-only
|
|
// at runtime by returning only the two landscape masks.
|
|
// Portrait is intentionally excluded: the grid-based inspection form is
|
|
// designed for landscape and does not adapt well to portrait on iPad.
|
|
|
|
final class AppDelegate: NSObject, UIApplicationDelegate {
|
|
func application(
|
|
_ application: UIApplication,
|
|
supportedInterfaceOrientationsFor window: UIWindow?
|
|
) -> UIInterfaceOrientationMask {
|
|
return [.landscapeLeft, .landscapeRight]
|
|
}
|
|
}
|
|
|
|
@main
|
|
struct JanitorialQCApp: App {
|
|
|
|
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
|
|
|
@StateObject private var auth = AuthManager.shared
|
|
@StateObject private var sync = SyncManager.shared
|
|
|
|
init() {
|
|
registerBackgroundTasks()
|
|
requestNotificationPermission()
|
|
// Set delegate so notifications display as banners when the app is in
|
|
// the foreground. Without this iOS silently drops them.
|
|
UNUserNotificationCenter.current().delegate = NotificationDelegate.shared
|
|
}
|
|
|
|
var body: some Scene {
|
|
WindowGroup {
|
|
ContentView()
|
|
.environmentObject(auth)
|
|
.environmentObject(sync)
|
|
}
|
|
.modelContainer(for: [
|
|
LocalFacility.self,
|
|
LocalArea.self,
|
|
LocalTemplate.self,
|
|
LocalInspection.self,
|
|
LocalIssue.self,
|
|
PendingPhoto.self,
|
|
SyncQueueEntry.self,
|
|
], isUndoEnabled: false) { result in
|
|
switch result {
|
|
case .success(let container):
|
|
// Only set the model context here — do NOT await anything.
|
|
// Session restore and sync are triggered by ContentView.task{}
|
|
// which runs on the MainActor inside the SwiftUI lifecycle,
|
|
// guaranteeing isLoading changes are seen by the view immediately.
|
|
SyncManager.shared.modelContext = container.mainContext
|
|
case .failure(let error):
|
|
fatalError("SwiftData container failed: \(error)")
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Local notification permission ─────────────────────────────────────
|
|
|
|
private func requestNotificationPermission() {
|
|
UNUserNotificationCenter.current().requestAuthorization(
|
|
options: [.alert, .sound, .badge]
|
|
) { granted, error in
|
|
if let error { print("[JQC] Notification permission error: \(error)") }
|
|
}
|
|
}
|
|
|
|
private func registerBackgroundTasks() {
|
|
BGTaskScheduler.shared.register(
|
|
forTaskWithIdentifier: "com.jqc.sync",
|
|
using: nil
|
|
) { task in
|
|
guard let processingTask = task as? BGProcessingTask else {
|
|
task.setTaskCompleted(success: false)
|
|
return
|
|
}
|
|
handleBackgroundSync(task: processingTask)
|
|
}
|
|
}
|
|
|
|
private func handleBackgroundSync(task: BGProcessingTask) {
|
|
scheduleBackgroundSync()
|
|
let syncTask = Task {
|
|
await SyncManager.shared.triggerSync()
|
|
}
|
|
task.expirationHandler = {
|
|
syncTask.cancel()
|
|
}
|
|
Task {
|
|
await syncTask.value
|
|
task.setTaskCompleted(success: !syncTask.isCancelled)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Notification delegate ─────────────────────────────────────────────────────
|
|
// Allows local notifications to appear as banners while the app is in the
|
|
// foreground. Without this delegate iOS discards them silently.
|
|
|
|
final class NotificationDelegate: NSObject, UNUserNotificationCenterDelegate {
|
|
static let shared = NotificationDelegate()
|
|
private override init() {}
|
|
|
|
func userNotificationCenter(
|
|
_ center: UNUserNotificationCenter,
|
|
willPresent notification: UNNotification,
|
|
withCompletionHandler completionHandler:
|
|
@escaping (UNNotificationPresentationOptions) -> Void
|
|
) {
|
|
// Show banner + play sound even when the app is active in foreground.
|
|
completionHandler([.banner, .sound])
|
|
}
|
|
}
|
|
|
|
func scheduleBackgroundSync() {
|
|
let request = BGProcessingTaskRequest(identifier: "com.jqc.sync")
|
|
request.requiresNetworkConnectivity = true
|
|
request.requiresExternalPower = false
|
|
try? BGTaskScheduler.shared.submit(request)
|
|
}
|