TrioApp.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. import ActivityKit
  2. import BackgroundTasks
  3. import CoreData
  4. import Foundation
  5. import SwiftUI
  6. import Swinject
  7. extension Notification.Name {
  8. static let initializationCompleted = Notification.Name("initializationCompleted")
  9. static let initializationError = Notification.Name("initializationError")
  10. }
  11. @main struct TrioApp: App {
  12. @Environment(\.scenePhase) var scenePhase
  13. @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
  14. // Read the color scheme preference from UserDefaults; defaults to system default setting
  15. @AppStorage("colorSchemePreference") private var colorSchemePreference: ColorSchemeOption = .systemDefault
  16. let coreDataStack = CoreDataStack.shared
  17. class InitState {
  18. var complete = false
  19. var error = false
  20. }
  21. // We use both InitState and @State variables to track coreDataStack
  22. // initialization. We need both to handle the cases when the coreDataStack
  23. // finishes before the UI and when it finishes after. SwiftUI doesn't have
  24. // clean mechanisms for handling background thread updates, thus this solution.
  25. let initState = InitState()
  26. @State private var appState = AppState()
  27. @State private var showLoadingView = true
  28. @State private var showLoadingError = false
  29. // Dependencies Assembler
  30. // contain all dependencies Assemblies
  31. // TODO: Remove static key after update "Use Dependencies" logic
  32. private static var assembler = Assembler([
  33. StorageAssembly(),
  34. ServiceAssembly(),
  35. APSAssembly(),
  36. NetworkAssembly(),
  37. UIAssembly(),
  38. SecurityAssembly()
  39. ], parent: nil, defaultObjectScope: .container)
  40. var resolver: Resolver {
  41. TrioApp.assembler.resolver
  42. }
  43. // Temp static var
  44. // Use to backward compatibility with old Dependencies logic on Logger
  45. // TODO: Remove var after update "Use Dependencies" logic in Logger
  46. static var resolver: Resolver {
  47. TrioApp.assembler.resolver
  48. }
  49. private func loadServices() {
  50. resolver.resolve(AppearanceManager.self)!.setupGlobalAppearance()
  51. _ = resolver.resolve(DeviceDataManager.self)!
  52. _ = resolver.resolve(APSManager.self)!
  53. _ = resolver.resolve(FetchGlucoseManager.self)!
  54. _ = resolver.resolve(FetchTreatmentsManager.self)!
  55. _ = resolver.resolve(CalendarManager.self)!
  56. _ = resolver.resolve(UserNotificationsManager.self)!
  57. _ = resolver.resolve(WatchManager.self)!
  58. _ = resolver.resolve(ContactImageManager.self)!
  59. _ = resolver.resolve(HealthKitManager.self)!
  60. _ = resolver.resolve(WatchManager.self)!
  61. _ = resolver.resolve(GarminManager.self)!
  62. _ = resolver.resolve(ContactImageManager.self)!
  63. _ = resolver.resolve(BluetoothStateManager.self)!
  64. _ = resolver.resolve(PluginManager.self)!
  65. _ = resolver.resolve(AlertPermissionsChecker.self)!
  66. if #available(iOS 16.2, *) {
  67. _ = resolver.resolve(LiveActivityManager.self)!
  68. }
  69. }
  70. init() {
  71. let submodulesInfo = BuildDetails.shared.submodules.map { key, value in
  72. "\(key): \(value.branch) \(value.commitSHA)"
  73. }.joined(separator: ", ")
  74. debug(
  75. .default,
  76. "Trio Started: v\(Bundle.main.releaseVersionNumber ?? "")(\(Bundle.main.buildVersionNumber ?? "")) [buildDate: \(String(describing: BuildDetails.shared.buildDate()))] [buildExpires: \(String(describing: BuildDetails.shared.calculateExpirationDate()))] [Branch: \(BuildDetails.shared.branchAndSha)] [submodules: \(submodulesInfo)]"
  77. )
  78. // Fix bug in iOS 18 related to the translucent tab bar
  79. configureTabBarAppearance()
  80. deferredInitialization()
  81. }
  82. /// Handles the deferred initialization of core components.
  83. ///
  84. /// Performs CoreDataStack initialization asynchronously and notifies the UI
  85. /// of completion or errors via notifications.
  86. private func deferredInitialization() {
  87. Task {
  88. do {
  89. try await coreDataStack.initializeStack()
  90. await Task { @MainActor in
  91. // Only load services after successful Core Data initialization
  92. loadServices()
  93. // Clear the persistentHistory and the NSManagedObjects that are older than 90 days every time the app starts
  94. cleanupOldData()
  95. self.initState.complete = true
  96. Foundation.NotificationCenter.default.post(name: .initializationCompleted, object: nil)
  97. UIApplication.shared.registerForRemoteNotifications()
  98. do {
  99. try await BuildDetails.shared.handleExpireDateChange()
  100. } catch {
  101. debug(.default, "Failed to handle expire date change: \(error)")
  102. }
  103. }.value
  104. } catch {
  105. debug(
  106. .coreData,
  107. "\(DebuggingIdentifiers.failed) Failed to initialize Core Data Stack: \(error.localizedDescription)"
  108. )
  109. await MainActor.run {
  110. self.initState.error = true
  111. Foundation.NotificationCenter.default.post(name: .initializationError, object: nil)
  112. }
  113. }
  114. }
  115. }
  116. /// Attempts to initialize the CoreDataStack again after a previous failure.
  117. ///
  118. /// Resets error states and triggers the initialization process from the beginning. Called in response
  119. /// to a UI "retry" button press from the Main.LoadingView
  120. private func retryCoreDataInitialization() {
  121. showLoadingError = false
  122. initState.error = false
  123. deferredInitialization()
  124. }
  125. var body: some Scene {
  126. WindowGroup {
  127. if self.showLoadingView {
  128. Main.LoadingView(showError: $showLoadingError, retry: retryCoreDataInitialization)
  129. .onAppear {
  130. if self.initState.complete {
  131. Task { @MainActor in
  132. try? await Task.sleep(for: .seconds(1.8))
  133. self.showLoadingView = false
  134. }
  135. }
  136. if self.initState.error {
  137. self.showLoadingError = true
  138. }
  139. }
  140. .onReceive(Foundation.NotificationCenter.default.publisher(for: .initializationCompleted)) { _ in
  141. Task { @MainActor in
  142. try? await Task.sleep(for: .seconds(1.8))
  143. self.showLoadingView = false
  144. }
  145. }
  146. .onReceive(Foundation.NotificationCenter.default.publisher(for: .initializationError)) { _ in
  147. self.showLoadingError = true
  148. }
  149. } else {
  150. Main.RootView(resolver: resolver)
  151. .preferredColorScheme(colorScheme(for: colorSchemePreference) ?? nil)
  152. .environment(\.managedObjectContext, coreDataStack.persistentContainer.viewContext)
  153. .environment(appState)
  154. .environmentObject(Icons())
  155. .onOpenURL(perform: handleURL)
  156. }
  157. }
  158. .onChange(of: scenePhase) { _, newScenePhase in
  159. debug(.default, "APPLICATION PHASE: \(newScenePhase)")
  160. /// If the App goes to the background we should ensure that all the changes are saved from the viewContext to the Persistent Container
  161. if newScenePhase == .background {
  162. coreDataStack.save()
  163. }
  164. if newScenePhase == .active {
  165. if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
  166. let rootVC = windowScene.windows.first(where: { $0.isKeyWindow })?.rootViewController
  167. {
  168. AppVersionChecker.shared.checkAndNotifyVersionStatus(in: rootVC)
  169. }
  170. if initState.complete {
  171. performCleanupIfNecessary()
  172. }
  173. }
  174. }
  175. }
  176. func configureTabBarAppearance() {
  177. let appearance = UITabBarAppearance()
  178. appearance.configureWithDefaultBackground()
  179. appearance.backgroundEffect = UIBlurEffect(style: .systemChromeMaterial)
  180. appearance.backgroundColor = UIColor.clear
  181. UITabBar.appearance().standardAppearance = appearance
  182. UITabBar.appearance().scrollEdgeAppearance = appearance
  183. }
  184. private func colorScheme(for colorScheme: ColorSchemeOption) -> ColorScheme? {
  185. switch colorScheme {
  186. case .systemDefault:
  187. return nil // Uses the system theme.
  188. case .light:
  189. return .light
  190. case .dark:
  191. return .dark
  192. }
  193. }
  194. private func performCleanupIfNecessary() {
  195. if let lastCleanupDate = UserDefaults.standard.object(forKey: "lastCleanupDate") as? Date {
  196. let sevenDaysAgo = Date().addingTimeInterval(-7 * 24 * 60 * 60)
  197. if lastCleanupDate < sevenDaysAgo {
  198. cleanupOldData()
  199. }
  200. }
  201. }
  202. private func cleanupOldData() {
  203. Task {
  204. async let cleanupTokens: () = coreDataStack.cleanupPersistentHistoryTokens(before: Date.oneWeekAgo)
  205. async let purgeData: () = purgeOldNSManagedObjects()
  206. await cleanupTokens
  207. try await purgeData
  208. // Update the last cleanup date
  209. UserDefaults.standard.set(Date(), forKey: "lastCleanupDate")
  210. }
  211. }
  212. private func purgeOldNSManagedObjects() async throws {
  213. async let glucoseDeletion: () = coreDataStack.batchDeleteOlderThan(GlucoseStored.self, dateKey: "date", days: 90)
  214. async let pumpEventDeletion: () = coreDataStack.batchDeleteOlderThan(PumpEventStored.self, dateKey: "timestamp", days: 90)
  215. async let bolusDeletion: () = coreDataStack.batchDeleteOlderThan(
  216. parentType: PumpEventStored.self,
  217. childType: BolusStored.self,
  218. dateKey: "timestamp",
  219. days: 90,
  220. relationshipKey: "pumpEvent"
  221. )
  222. async let tempBasalDeletion: () = coreDataStack.batchDeleteOlderThan(
  223. parentType: PumpEventStored.self,
  224. childType: TempBasalStored.self,
  225. dateKey: "timestamp",
  226. days: 90,
  227. relationshipKey: "pumpEvent"
  228. )
  229. async let determinationDeletion: () = coreDataStack
  230. .batchDeleteOlderThan(OrefDetermination.self, dateKey: "deliverAt", days: 90)
  231. async let batteryDeletion: () = coreDataStack.batchDeleteOlderThan(OpenAPS_Battery.self, dateKey: "date", days: 90)
  232. async let carbEntryDeletion: () = coreDataStack.batchDeleteOlderThan(CarbEntryStored.self, dateKey: "date", days: 90)
  233. async let forecastDeletion: () = coreDataStack.batchDeleteOlderThan(Forecast.self, dateKey: "date", days: 2)
  234. async let forecastValueDeletion: () = coreDataStack.batchDeleteOlderThan(
  235. parentType: Forecast.self,
  236. childType: ForecastValue.self,
  237. dateKey: "date",
  238. days: 2,
  239. relationshipKey: "forecast"
  240. )
  241. async let overrideDeletion: () = coreDataStack
  242. .batchDeleteOlderThan(OverrideStored.self, dateKey: "date", days: 3, isPresetKey: "isPreset")
  243. async let overrideRunDeletion: () = coreDataStack
  244. .batchDeleteOlderThan(OverrideRunStored.self, dateKey: "startDate", days: 3)
  245. // Await each task to ensure they are all completed
  246. try await glucoseDeletion
  247. try await pumpEventDeletion
  248. try await bolusDeletion
  249. try await tempBasalDeletion
  250. try await determinationDeletion
  251. try await batteryDeletion
  252. try await carbEntryDeletion
  253. try await forecastDeletion
  254. try await forecastValueDeletion
  255. try await overrideDeletion
  256. try await overrideRunDeletion
  257. }
  258. private func handleURL(_ url: URL) {
  259. let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
  260. switch components?.host {
  261. case "device-select-resp":
  262. resolver.resolve(NotificationCenter.self)!.post(name: .openFromGarminConnect, object: url)
  263. default: break
  264. }
  265. }
  266. }