MainStateModel.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  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 showAlertMessage(_ message: MessageContent) {
  101. if message.useAPN, !alertPermissionsChecker.notificationsDisabled
  102. {
  103. showAPN(message)
  104. } else {
  105. showSwiftMessage(message)
  106. }
  107. }
  108. private func showAPN(_ message: MessageContent) {
  109. DispatchQueue.main.async {
  110. self.broadcaster.notify(alertMessageNotificationObserver.self, on: .main) {
  111. $0.alertMessageNotification(message)
  112. }
  113. }
  114. }
  115. // Read the color scheme preference from UserDefaults; defaults to system default setting
  116. @AppStorage("colorSchemePreference") private var colorSchemePreference: ColorSchemeOption = .systemDefault
  117. private func showSwiftMessage(_ message: MessageContent) {
  118. if snoozeUntilDate > Date(), message.action == .snooze {
  119. return
  120. }
  121. var config = SwiftMessages.defaultConfig
  122. let view = MessageView.viewFromNib(layout: .cardView)
  123. view.layoutMargins = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
  124. config.prefersStatusBarHidden = true
  125. // Set id so that multiple notifications are not queued while waiting for user response; only the latest will be shown
  126. if message.subtype == .glucose || message.subtype == .carb {
  127. view.id = message.type.rawValue + message.subtype.rawValue
  128. }
  129. let titleContent: String
  130. let iconName = UIApplication.shared.alternateIconName ?? "trioBlack"
  131. let iconImage = UIImage(named: iconName) ?? UIImage()
  132. view.configureContent(
  133. title: "title",
  134. body: NSLocalizedString(message.content, comment: "Info message"),
  135. iconImage: nil,
  136. iconText: nil,
  137. buttonImage: nil,
  138. buttonTitle: nil,
  139. buttonTapHandler: nil
  140. )
  141. view.configureIcon(withSize: CGSize(width: 40, height: 40), contentMode: .scaleAspectFit)
  142. view.iconImageView!.image = iconImage
  143. view.iconImageView?.layer.cornerRadius = 10
  144. view.customConfigureTheme(
  145. colorSchemePreference: colorSchemePreference
  146. )
  147. view.iconImageView?.image = iconImage
  148. switch message.type {
  149. case .info,
  150. .other:
  151. config.duration = .seconds(seconds: 5)
  152. titleContent = message.title != "" ? message.title : NSLocalizedString("Info", comment: "Info title")
  153. case .warning:
  154. config.duration = .forever
  155. titleContent = message.title != "" ? message
  156. .title : NSLocalizedString("Warning", comment: "Warning title")
  157. case .error:
  158. config.duration = .forever
  159. titleContent = message.title != "" ? message
  160. .title : NSLocalizedString("Error", comment: "Error title")
  161. }
  162. view.titleLabel?.text = titleContent
  163. config.dimMode = .gray(interactive: true)
  164. setupAction(message: message, view: view)
  165. if message.trigger != nil {
  166. addOrReplaceTriggerTimer(message: message, config: config, view: view)
  167. }
  168. guard message.type == .error || message.action != .pumpConfig, message.trigger == nil, !view.isHidden else { return }
  169. SwiftMessages.show(config: config, view: view)
  170. }
  171. override func subscribe() {
  172. router.mainModalScreen
  173. .map { $0?.modal(resolver: self.resolver!) }
  174. .removeDuplicates { $0?.id == $1?.id }
  175. .receive(on: DispatchQueue.main)
  176. .sink { modal in
  177. self.modal = modal
  178. self.isModalPresented = modal != nil
  179. }
  180. .store(in: &lifetime)
  181. $isModalPresented
  182. .filter { !$0 }
  183. .sink { _ in
  184. self.router.mainModalScreen.send(nil)
  185. }
  186. .store(in: &lifetime)
  187. router.alertMessage
  188. .receive(on: DispatchQueue.main)
  189. .sink { message in
  190. guard !self.isApnPumpConfigAction(message) else { return }
  191. guard self.router.allowNotify(message, self.settingsManager.settings) else { return }
  192. self.showAlertMessage(message)
  193. }
  194. .store(in: &lifetime)
  195. router.mainSecondaryModalView
  196. .receive(on: DispatchQueue.main)
  197. .sink { view in
  198. self.secondaryModalView = view
  199. self.isSecondaryModalPresented = view != nil
  200. }
  201. .store(in: &lifetime)
  202. $isSecondaryModalPresented
  203. .removeDuplicates()
  204. .filter { !$0 }
  205. .sink { _ in
  206. self.router.mainSecondaryModalView.send(nil)
  207. }
  208. .store(in: &lifetime)
  209. }
  210. }
  211. }
  212. extension MessageView {
  213. func currentColorScheme() -> ColorScheme {
  214. let userInterfaceStyle = UITraitCollection.current.userInterfaceStyle
  215. return userInterfaceStyle == .dark ? .dark : .light
  216. }
  217. func customConfigureTheme(colorSchemePreference: ColorSchemeOption) {
  218. let defaultSystemColorScheme = currentColorScheme()
  219. var backgroundColor = UIColor.systemBackground
  220. var foregroundColor = UIColor.white
  221. let ApnBackground = UIColor(named: "ApnBackground") ?? UIColor.lightGray
  222. let iOSlightTrioDark = UIColor(named: "ApnBackgroundLightDark") ?? UIColor.lightGray
  223. switch colorSchemePreference {
  224. case .systemDefault:
  225. backgroundColor = ApnBackground
  226. foregroundColor = UIColor.label
  227. case .dark:
  228. backgroundColor = defaultSystemColorScheme == .light ? iOSlightTrioDark : ApnBackground
  229. foregroundColor = defaultSystemColorScheme == .light ? UIColor.black : UIColor.white
  230. case .light:
  231. backgroundColor = defaultSystemColorScheme == .light ? ApnBackground : UIColor.gray
  232. foregroundColor = defaultSystemColorScheme == .light ? UIColor.black : UIColor.white
  233. }
  234. iconImageView?.tintColor = foregroundColor
  235. backgroundView.backgroundColor = backgroundColor
  236. titleLabel?.textColor = foregroundColor
  237. bodyLabel?.textColor = foregroundColor
  238. iconImageView?.isHidden = iconImageView?.image == nil
  239. backgroundView.layer.cornerRadius = 25
  240. let adjustedFont = UIFont.systemFont(ofSize: 13.0, weight: .bold)
  241. let preferredTitleFont = UIFontMetrics(forTextStyle: .footnote).scaledFont(for: adjustedFont)
  242. let preferredBodyFont = UIFont.preferredFontforStyle(forTextStyle: .footnote)
  243. // Set the title and body font to the dynamic type sizes
  244. titleLabel?.adjustsFontForContentSizeCategory = true
  245. titleLabel?.font = preferredTitleFont
  246. bodyLabel?.adjustsFontForContentSizeCategory = true
  247. bodyLabel?.font = preferredBodyFont
  248. // Set custom colors for title and body text
  249. titleLabel?.textColor = foregroundColor
  250. bodyLabel?.textColor = foregroundColor
  251. }
  252. }
  253. @available(iOS 16.0, *)
  254. extension Main.StateModel: CompletionDelegate {
  255. func completionNotifyingDidComplete(_: CompletionNotifying) {
  256. // close the window
  257. router.mainSecondaryModalView.send(nil)
  258. }
  259. }
  260. // Extension to convert SwiftUI TextStyle to UIFont
  261. extension UIFont {
  262. static func preferredFontforStyle(forTextStyle: UIFont.TextStyle) -> UIFont {
  263. let uiFontMetrics = UIFontMetrics.default
  264. let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: forTextStyle)
  265. return uiFontMetrics.scaledFont(for: UIFont(descriptor: descriptor, size: 0))
  266. }
  267. }