SceneDelegate.swift 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. // LoopFollow
  2. // SceneDelegate.swift
  3. import AVFoundation
  4. import SwiftUI
  5. import UIKit
  6. class SceneDelegate: UIResponder, UIWindowSceneDelegate {
  7. var window: UIWindow?
  8. let synthesizer = AVSpeechSynthesizer()
  9. /// One-shot guard so the consent prompt is only attempted once per
  10. /// process lifetime even if the scene activates repeatedly.
  11. private var consentPromptShownThisProcess = false
  12. func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) {
  13. // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
  14. // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
  15. // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
  16. guard let _ = (scene as? UIWindowScene) else { return }
  17. // get the tabBar
  18. guard let tabBarController = window?.rootViewController as? UITabBarController,
  19. let viewControllers = tabBarController.viewControllers
  20. else {
  21. return
  22. }
  23. }
  24. func sceneDidDisconnect(_: UIScene) {
  25. // Called as the scene is being released by the system.
  26. // This occurs shortly after the scene enters the background, or when its session is discarded.
  27. // Release any resources associated with this scene that can be re-created the next time the scene connects.
  28. // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
  29. }
  30. func sceneDidBecomeActive(_: UIScene) {
  31. // Called when the scene has moved from an inactive state to an active state.
  32. // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
  33. runTelemetryFirstForegroundHook()
  34. }
  35. /// Presents the one-time consent sheet on first foreground. Sending is
  36. /// handled by AppDelegate at launch and by TaskScheduler thereafter —
  37. /// firing maybeSend here would duplicate the launch-time send.
  38. private func runTelemetryFirstForegroundHook() {
  39. if !Storage.shared.telemetryConsentDecisionMade.value,
  40. !consentPromptShownThisProcess
  41. {
  42. consentPromptShownThisProcess = true
  43. presentTelemetryConsentSheet()
  44. }
  45. }
  46. private func presentTelemetryConsentSheet() {
  47. guard let root = window?.rootViewController else { return }
  48. // Find the topmost presented controller so we don't try to present
  49. // over a sheet that's already up.
  50. var top = root
  51. while let presented = top.presentedViewController {
  52. top = presented
  53. }
  54. let host = UIHostingController(rootView: TelemetryConsentView())
  55. host.isModalInPresentation = true // user must explicitly choose
  56. // Defer to the next runloop so view hierarchy is settled when the
  57. // scene first becomes active on a fresh install.
  58. DispatchQueue.main.async {
  59. top.present(host, animated: true)
  60. }
  61. }
  62. func scene(_: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
  63. guard URLContexts.contains(where: { $0.url.scheme == AppGroupID.urlScheme && $0.url.host == "la-tap" }) else { return }
  64. // scene(_:openURLContexts:) fires after sceneDidBecomeActive when the app
  65. // foregrounds from background. Post on the next run loop so the view
  66. // hierarchy (including any presented modals) is fully settled.
  67. #if !targetEnvironment(macCatalyst)
  68. DispatchQueue.main.async {
  69. NotificationCenter.default.post(name: .liveActivityDidForeground, object: nil)
  70. }
  71. #endif
  72. }
  73. func sceneWillResignActive(_: UIScene) {
  74. // Called when the scene will move from an active state to an inactive state.
  75. // This may occur due to temporary interruptions (ex. an incoming phone call).
  76. }
  77. func sceneWillEnterForeground(_: UIScene) {
  78. // Called as the scene transitions from the background to the foreground.
  79. // Use this method to undo the changes made on entering the background.
  80. }
  81. func sceneDidEnterBackground(_: UIScene) {
  82. // Called as the scene transitions from the foreground to the background.
  83. // Use this method to save data, release shared resources, and store enough scene-specific state information
  84. // to restore the scene back to its current state.
  85. // Save changes in the application's managed object context when the application transitions to the background.
  86. (UIApplication.shared.delegate as? AppDelegate)?.saveContext()
  87. }
  88. /// Handle the UIApplicationShortcutItem when the user taps on the Home Screen Quick Action. This function toggles the "Speak BG" setting in UserDefaultsRepository, speaks the current state (on/off) using AVSpeechSynthesizer, and updates the Quick Action appearance.
  89. func handleShortcutItem(_ shortcutItem: UIApplicationShortcutItem) {
  90. if let bundleIdentifier = Bundle.main.bundleIdentifier {
  91. let expectedType = bundleIdentifier + ".toggleSpeakBG"
  92. if shortcutItem.type == expectedType {
  93. Storage.shared.speakBG.value.toggle()
  94. let message = Storage.shared.speakBG.value ? "BG Speak is now on" : "BG Speak is now off"
  95. let utterance = AVSpeechUtterance(string: message)
  96. synthesizer.speak(utterance)
  97. }
  98. }
  99. }
  100. /// The following method is called when the user taps on the Home Screen Quick Action
  101. func windowScene(_: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler _: @escaping (Bool) -> Void) {
  102. handleShortcutItem(shortcutItem)
  103. }
  104. }