MainStateModel.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. import Combine
  2. import LoopKitUI
  3. import SwiftMessages
  4. import SwiftUI
  5. import Swinject
  6. extension Main {
  7. final class StateModel: BaseStateModel<Provider> {
  8. @Injected() var alertPermissionsChecker: AlertPermissionsChecker!
  9. @Injected() var broadcaster: Broadcaster!
  10. private(set) var modal: Modal?
  11. @Published var isModalPresented = false
  12. @Published var isSecondaryModalPresented = false
  13. @Published var secondaryModalView: AnyView? = nil
  14. @Persisted(key: "UserNotificationsManager.snoozeUntilDate") private var snoozeUntilDate: Date = .distantPast
  15. private var timers: [TimeInterval: Timer] = [:]
  16. private func showTriggeredView(
  17. message: MessageContent,
  18. interval _: TimeInterval,
  19. config: SwiftMessages.Config,
  20. view: MessageView
  21. ) {
  22. view.customConfigureTheme(
  23. colorSchemePreference: colorSchemePreference
  24. )
  25. setupAction(message: message, view: view)
  26. SwiftMessages.show(config: config, view: view)
  27. }
  28. // Add or replace timer for a specific TimeInterval
  29. private func addOrReplaceTriggerTimer(message: MessageContent, config: SwiftMessages.Config, view: MessageView) {
  30. let trigger = message.trigger as! UNTimeIntervalNotificationTrigger
  31. guard trigger.timeInterval > 0 else { return }
  32. let interval = trigger.timeInterval
  33. SwiftMessages.hide(id: view.id)
  34. // If a timer already exists for this interval, invalidate it
  35. if let existingTimer = timers[interval] {
  36. existingTimer.invalidate()
  37. }
  38. // Create a new timer with the provided interval
  39. let newTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: false) { [weak self] _ in
  40. self?.showTriggeredView(message: message, interval: interval, config: config, view: view)
  41. self?.timers[interval] = nil
  42. }
  43. timers[interval] = newTimer
  44. }
  45. // Cancel all timers (optional cleanup method)
  46. private func cancelAllTimers() {
  47. timers.values.forEach { $0.invalidate() }
  48. timers.removeAll()
  49. }
  50. private func setupPumpConfig() {
  51. // display the pump configuration immediatly
  52. if let pump = provider.deviceManager.pumpManager,
  53. let bluetooth = provider.bluetoothProvider
  54. {
  55. let view = PumpConfig.PumpSettingsView(
  56. pumpManager: pump,
  57. bluetoothManager: bluetooth,
  58. completionDelegate: self
  59. ).asAny()
  60. router.mainSecondaryModalView.send(view)
  61. }
  62. }
  63. private func setupButton(message _: MessageContent, view: MessageView) {
  64. view.button?.setImage(UIImage(), for: .normal)
  65. view.iconLabel = nil
  66. let buttonImage = UIImage(systemName: "chevron.right")?.withTintColor(.white)
  67. view.button?.setImage(buttonImage, for: .normal)
  68. view.button?.backgroundColor = view.backgroundView.backgroundColor
  69. view.button?.tintColor = view.iconImageView?.tintColor
  70. }
  71. private func setupAction(message: MessageContent, view: MessageView) {
  72. switch message.action {
  73. case .snooze:
  74. setupButton(message: message, view: view)
  75. view.buttonTapHandler = { _ in
  76. // Popup Snooze view when user taps on Glucose Notification
  77. SwiftMessages.hide()
  78. self.router.mainModalScreen.send(.snooze)
  79. }
  80. case .pumpConfig:
  81. setupButton(message: message, view: view)
  82. view.buttonTapHandler = { _ in
  83. SwiftMessages.hide()
  84. self.setupPumpConfig()
  85. }
  86. default: // break
  87. view.button?.setImage(UIImage(), for: .normal)
  88. view.buttonTapHandler = { _ in
  89. SwiftMessages.hide()
  90. }
  91. }
  92. }
  93. private func isApnPumpConfigAction(_ message: MessageContent) -> Bool {
  94. if message.type != .error, message.action == .pumpConfig {
  95. setupPumpConfig()
  96. return true
  97. }
  98. return false
  99. }
  100. private func allowNotify(_ message: MessageContent) -> Bool {
  101. if message.type == .error { return true } // .errorPump
  102. switch message.subtype {
  103. case .pump:
  104. guard settingsManager.settings.notificationsPump else { return false }
  105. case .cgm:
  106. guard settingsManager.settings.notificationsCgm else { return false }
  107. case .carb:
  108. guard settingsManager.settings.notificationsCarb else { return false }
  109. case .glucose:
  110. guard settingsManager.settings.glucoseNotificationsAlways else { return false }
  111. case .algorithm:
  112. guard settingsManager.settings.notificationsAlgorithm else { return false }
  113. case .misc:
  114. return true
  115. }
  116. return true
  117. }
  118. private func showAlertMessage(_ message: MessageContent) {
  119. if message.useAPN, !alertPermissionsChecker.notificationsDisabled
  120. {
  121. showAPN(message)
  122. } else {
  123. showSwiftMessage(message)
  124. }
  125. }
  126. private func showAPN(_ message: MessageContent) {
  127. DispatchQueue.main.async {
  128. self.broadcaster.notify(alertMessageNotificationObserver.self, on: .main) {
  129. $0.alertMessageNotification(message)
  130. }
  131. }
  132. }
  133. // Read the color scheme preference from UserDefaults; defaults to system default setting
  134. @AppStorage("colorSchemePreference") private var colorSchemePreference: ColorSchemeOption = .systemDefault
  135. private func showSwiftMessage(_ message: MessageContent) {
  136. if snoozeUntilDate > Date(), message.action == .snooze {
  137. return
  138. }
  139. var config = SwiftMessages.defaultConfig
  140. let view = MessageView.viewFromNib(layout: .cardView)
  141. view.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
  142. config.prefersStatusBarHidden = true
  143. // Set id so that multiple notifications are not queued while waiting for user response; only the latest will be shown
  144. if message.subtype == .glucose || message.subtype == .carb {
  145. view.id = message.type.rawValue + message.subtype.rawValue
  146. }
  147. let titleContent: String
  148. let iconName = UIApplication.shared.alternateIconName ?? "trioBlack"
  149. let iconImage = UIImage(named: iconName) ?? UIImage()
  150. view.configureContent(
  151. title: "title",
  152. body: NSLocalizedString(message.content, comment: "Info message"),
  153. iconImage: nil,
  154. iconText: nil,
  155. buttonImage: nil,
  156. buttonTitle: nil,
  157. buttonTapHandler: nil
  158. )
  159. view.configureIcon(withSize: CGSize(width: 40, height: 40), contentMode: .scaleAspectFit)
  160. view.iconImageView!.image = iconImage
  161. view.iconImageView?.layer.cornerRadius = 10
  162. view.customConfigureTheme(
  163. colorSchemePreference: colorSchemePreference
  164. )
  165. view.iconImageView?.image = iconImage
  166. switch message.type {
  167. case .info,
  168. .other:
  169. config.duration = .seconds(seconds: 5)
  170. titleContent = message.title != "" ? message.title : NSLocalizedString("Info", comment: "Info title")
  171. case .warning:
  172. config.duration = .forever
  173. titleContent = message.title != "" ? message
  174. .title : NSLocalizedString("Warning", comment: "Warning title")
  175. case .error:
  176. config.duration = .forever
  177. titleContent = message.title != "" ? message
  178. .title : NSLocalizedString("Error", comment: "Error title")
  179. }
  180. view.titleLabel?.text = titleContent
  181. config.dimMode = .gray(interactive: true)
  182. setupAction(message: message, view: view)
  183. if message.trigger != nil {
  184. addOrReplaceTriggerTimer(message: message, config: config, view: view)
  185. }
  186. guard message.type == .error || message.action != .pumpConfig, message.trigger == nil, !view.isHidden else { return }
  187. SwiftMessages.show(config: config, view: view)
  188. }
  189. override func subscribe() {
  190. router.mainModalScreen
  191. .map { $0?.modal(resolver: self.resolver!) }
  192. .removeDuplicates { $0?.id == $1?.id }
  193. .receive(on: DispatchQueue.main)
  194. .sink { modal in
  195. self.modal = modal
  196. self.isModalPresented = modal != nil
  197. }
  198. .store(in: &lifetime)
  199. $isModalPresented
  200. .filter { !$0 }
  201. .sink { _ in
  202. self.router.mainModalScreen.send(nil)
  203. }
  204. .store(in: &lifetime)
  205. router.alertMessage
  206. .receive(on: DispatchQueue.main)
  207. .sink { message in
  208. guard !self.isApnPumpConfigAction(message) else { return }
  209. guard self.allowNotify(message) else { return }
  210. self.showAlertMessage(message)
  211. }
  212. .store(in: &lifetime)
  213. router.mainSecondaryModalView
  214. .receive(on: DispatchQueue.main)
  215. .sink { view in
  216. self.secondaryModalView = view
  217. self.isSecondaryModalPresented = view != nil
  218. }
  219. .store(in: &lifetime)
  220. $isSecondaryModalPresented
  221. .removeDuplicates()
  222. .filter { !$0 }
  223. .sink { _ in
  224. self.router.mainSecondaryModalView.send(nil)
  225. }
  226. .store(in: &lifetime)
  227. }
  228. }
  229. }
  230. extension MessageView {
  231. func currentColorScheme() -> ColorScheme {
  232. let userInterfaceStyle = UITraitCollection.current.userInterfaceStyle
  233. return userInterfaceStyle == .dark ? .dark : .light
  234. }
  235. func customConfigureTheme(colorSchemePreference: ColorSchemeOption) {
  236. let defaultSystemColorScheme = currentColorScheme()
  237. var backgroundColor = UIColor.systemBackground
  238. var foregroundColor = UIColor.white
  239. let ApnBackground = UIColor(named: "ApnBackground") ?? UIColor.lightGray
  240. let iOSlightTrioDark = UIColor(named: "ApnBackgroundLightDark") ?? UIColor.lightGray
  241. switch colorSchemePreference {
  242. case .systemDefault:
  243. backgroundColor = ApnBackground
  244. foregroundColor = UIColor.label
  245. case .dark:
  246. backgroundColor = defaultSystemColorScheme == .light ? iOSlightTrioDark : ApnBackground
  247. foregroundColor = defaultSystemColorScheme == .light ? UIColor.black : UIColor.white
  248. case .light:
  249. backgroundColor = defaultSystemColorScheme == .light ? ApnBackground : UIColor.gray
  250. foregroundColor = defaultSystemColorScheme == .light ? UIColor.black : UIColor.white
  251. }
  252. iconImageView?.tintColor = foregroundColor
  253. backgroundView.backgroundColor = backgroundColor
  254. titleLabel?.textColor = foregroundColor
  255. bodyLabel?.textColor = foregroundColor
  256. iconImageView?.isHidden = iconImageView?.image == nil
  257. backgroundView.layer.cornerRadius = 25
  258. let adjustedFont = UIFont.systemFont(ofSize: 13.0, weight: .bold)
  259. let preferredTitleFont = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: adjustedFont)
  260. let preferredBodyFont = UIFont.preferredFontforStyle(forTextStyle: .footnote)
  261. // Set the title and body font to the dynamic type sizes
  262. titleLabel?.adjustsFontForContentSizeCategory = true
  263. titleLabel?.font = preferredTitleFont
  264. bodyLabel?.adjustsFontForContentSizeCategory = true
  265. bodyLabel?.font = preferredBodyFont
  266. // Set custom colors for title and body text
  267. titleLabel?.textColor = foregroundColor
  268. bodyLabel?.textColor = foregroundColor
  269. }
  270. }
  271. @available(iOS 16.0, *)
  272. extension Main.StateModel: CompletionDelegate {
  273. func completionNotifyingDidComplete(_: CompletionNotifying) {
  274. // close the window
  275. router.mainSecondaryModalView.send(nil)
  276. }
  277. }
  278. // Extension to convert SwiftUI TextStyle to UIFont
  279. extension UIFont {
  280. static func preferredFontforStyle(forTextStyle: UIFont.TextStyle) -> UIFont {
  281. let uiFontMetrics = UIFontMetrics.default
  282. let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: forTextStyle)
  283. return uiFontMetrics.scaledFont(for: UIFont(descriptor: descriptor, size: 0))
  284. }
  285. }