TrioApp.swift 13 KB

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