InsulinModelSelection.swift 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. //
  2. // InsulinModelSelection.swift
  3. // Loop
  4. //
  5. // Created by Michael Pangburn on 7/14/20.
  6. // Copyright © 2020 LoopKit Authors. All rights reserved.
  7. //
  8. import HealthKit
  9. import SwiftUI
  10. import LoopKit
  11. public struct InsulinModelSelection: View {
  12. @Environment(\.appName) private var appName
  13. @Environment(\.dismiss) var dismiss
  14. @Environment(\.authenticate) var authenticate
  15. let initialValue: InsulinModelSettings
  16. @State var value: InsulinModelSettings
  17. let insulinSensitivitySchedule: InsulinSensitivitySchedule
  18. let glucoseUnit: HKUnit
  19. let supportedModelSettings: SupportedInsulinModelSettings
  20. let mode: SettingsPresentationMode
  21. let save: (_ insulinModelSettings: InsulinModelSettings) -> Void
  22. let chartManager: ChartsManager
  23. static let defaultInsulinSensitivitySchedule = InsulinSensitivitySchedule(unit: .milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue<Double>(startTime: 0, value: 40)])!
  24. public init(
  25. value: InsulinModelSettings,
  26. insulinSensitivitySchedule: InsulinSensitivitySchedule?,
  27. glucoseUnit: HKUnit,
  28. supportedModelSettings: SupportedInsulinModelSettings,
  29. chartColors: ChartColorPalette,
  30. onSave save: @escaping (_ insulinModelSettings: InsulinModelSettings) -> Void,
  31. mode: SettingsPresentationMode
  32. ){
  33. self._value = State(initialValue: value)
  34. self.initialValue = value
  35. self.insulinSensitivitySchedule = insulinSensitivitySchedule ?? Self.defaultInsulinSensitivitySchedule
  36. self.save = save
  37. self.glucoseUnit = glucoseUnit
  38. self.supportedModelSettings = supportedModelSettings
  39. self.mode = mode
  40. self.chartManager = {
  41. let chartManager = ChartsManager(
  42. colors: chartColors,
  43. settings: .default,
  44. axisLabelFont: .systemFont(ofSize: 12),
  45. charts: [InsulinModelChart()],
  46. traitCollection: .current
  47. )
  48. chartManager.startDate = Calendar.current.nextDate(
  49. after: Date(),
  50. matching: DateComponents(minute: 0),
  51. matchingPolicy: .strict,
  52. direction: .backward
  53. ) ?? Date()
  54. return chartManager
  55. }()
  56. }
  57. public init(
  58. viewModel: TherapySettingsViewModel,
  59. didSave: (() -> Void)? = nil
  60. ) {
  61. self.init(
  62. value: viewModel.therapySettings.insulinModelSettings ?? InsulinModelSettings.exponentialPreset(.rapidActingAdult),
  63. insulinSensitivitySchedule: viewModel.therapySettings.insulinSensitivitySchedule,
  64. glucoseUnit: viewModel.therapySettings.insulinSensitivitySchedule?.unit ?? viewModel.preferredGlucoseUnit,
  65. supportedModelSettings: viewModel.supportedInsulinModelSettings,
  66. chartColors: viewModel.chartColors,
  67. onSave: { [weak viewModel] insulinModelSettings in
  68. viewModel?.saveInsulinModel(insulinModelSettings: insulinModelSettings)
  69. didSave?()
  70. },
  71. mode: viewModel.mode
  72. )
  73. }
  74. public var body: some View {
  75. switch mode {
  76. case .acceptanceFlow: return AnyView(content)
  77. case .settings: return AnyView(contentWithCancel)
  78. }
  79. }
  80. private var contentWithCancel: some View {
  81. if value == initialValue {
  82. return AnyView(content
  83. .navigationBarBackButtonHidden(false)
  84. .navigationBarItems(leading: EmptyView())
  85. )
  86. } else {
  87. return AnyView(content
  88. .navigationBarBackButtonHidden(true)
  89. .navigationBarItems(leading: cancelButton)
  90. )
  91. }
  92. }
  93. private var cancelButton: some View {
  94. Button(action: { self.dismiss() } ) { Text(LocalizedString("Cancel", comment: "Cancel editing settings button title")) }
  95. }
  96. private var content: some View {
  97. VStack(spacing: 0) {
  98. list
  99. Button(action: { self.startSaving() }) {
  100. Text(mode.buttonText)
  101. .actionButtonStyle(.primary)
  102. .padding()
  103. }
  104. .disabled(value == initialValue && mode != .acceptanceFlow)
  105. // Styling to mimic the floating button of a ConfigurationPage
  106. .padding(.bottom)
  107. .background(Color(.secondarySystemGroupedBackground).shadow(radius: 5))
  108. }
  109. .navigationBarTitle(Text(TherapySetting.insulinModel.title), displayMode: .large)
  110. .supportedInterfaceOrientations(.portrait)
  111. .edgesIgnoringSafeArea(.bottom)
  112. }
  113. private var list: some View {
  114. List {
  115. Section {
  116. SettingDescription(
  117. text: insulinModelSettingDescription,
  118. informationalContent: {
  119. TherapySetting.insulinModel.helpScreen()
  120. }
  121. )
  122. .padding(4)
  123. .padding(.top, 4)
  124. VStack {
  125. InsulinModelChartView(
  126. chartManager: chartManager,
  127. glucoseUnit: glucoseUnit,
  128. selectedInsulinModelValues: selectedInsulinModelValues,
  129. unselectedInsulinModelValues: unselectedInsulinModelValues,
  130. glucoseDisplayRange: endingGlucoseQuantity...startingGlucoseQuantity
  131. )
  132. .frame(height: 170)
  133. CheckmarkListItem(
  134. title: Text(InsulinModelSettings.exponentialPreset(.rapidActingAdult).title),
  135. description: Text(InsulinModelSettings.exponentialPreset(.rapidActingAdult).subtitle),
  136. isSelected: isSelected(.exponentialPreset(.rapidActingAdult))
  137. )
  138. .padding(.vertical, 4)
  139. }
  140. CheckmarkListItem(
  141. title: Text(InsulinModelSettings.exponentialPreset(.rapidActingChild).title),
  142. description: Text(InsulinModelSettings.exponentialPreset(.rapidActingChild).subtitle),
  143. isSelected: isSelected(.exponentialPreset(.rapidActingChild))
  144. )
  145. .padding(.vertical, 4)
  146. .padding(.bottom, 4)
  147. }
  148. .buttonStyle(PlainButtonStyle()) // Disable row highlighting on selection
  149. }
  150. .insetGroupedListStyle()
  151. }
  152. var insulinModelSettingDescription: Text {
  153. let spellOutFormatter = NumberFormatter()
  154. spellOutFormatter.numberStyle = .spellOut
  155. let modelCountString = spellOutFormatter.string(from: selectableInsulinModelSettings.count as NSNumber)!
  156. return Text(String(format: LocalizedString("For fast acting insulin, %1$@ assumes it is actively working for 6 hours. You can choose from %2$@ different models for how the app measures the insulin’s peak activity.", comment: "Insulin model setting description (1: app name) (2: number of models)"), appName, modelCountString))
  157. }
  158. var insulinModelChart: InsulinModelChart {
  159. chartManager.charts.first! as! InsulinModelChart
  160. }
  161. var selectableInsulinModelSettings: [InsulinModelSettings] {
  162. var options: [InsulinModelSettings] = [
  163. .exponentialPreset(.rapidActingAdult),
  164. .exponentialPreset(.rapidActingChild)
  165. ]
  166. return options
  167. }
  168. private var selectedInsulinModelValues: [GlucoseValue] {
  169. oneUnitBolusEffectPrediction(using: value)
  170. }
  171. private var unselectedInsulinModelValues: [[GlucoseValue]] {
  172. selectableInsulinModelSettings
  173. .filter { $0 != value }
  174. .map { oneUnitBolusEffectPrediction(using: $0) }
  175. }
  176. private func oneUnitBolusEffectPrediction(using modelSettings: InsulinModelSettings) -> [GlucoseValue] {
  177. let bolus = DoseEntry(type: .bolus, startDate: chartManager.startDate, value: 1, unit: .units, insulinType: .novolog)
  178. let startingGlucoseSample = HKQuantitySample(type: HKQuantityType.quantityType(forIdentifier: .bloodGlucose)!, quantity: startingGlucoseQuantity, start: chartManager.startDate, end: chartManager.startDate)
  179. let effects = [bolus].glucoseEffects(insulinModelSettings: modelSettings, insulinSensitivity: insulinSensitivitySchedule)
  180. return LoopMath.predictGlucose(startingAt: startingGlucoseSample, effects: effects)
  181. }
  182. private var startingGlucoseQuantity: HKQuantity {
  183. let startingGlucoseValue = insulinSensitivitySchedule.quantity(at: chartManager.startDate).doubleValue(for: glucoseUnit) + glucoseUnit.glucoseExampleTargetValue
  184. return HKQuantity(unit: glucoseUnit, doubleValue: startingGlucoseValue)
  185. }
  186. private var endingGlucoseQuantity: HKQuantity {
  187. HKQuantity(unit: glucoseUnit, doubleValue: glucoseUnit.glucoseExampleTargetValue)
  188. }
  189. private func isSelected(_ settings: InsulinModelSettings) -> Binding<Bool> {
  190. Binding(
  191. get: { self.value == settings },
  192. set: { isSelected in
  193. if isSelected {
  194. withAnimation {
  195. self.value = settings
  196. }
  197. }
  198. }
  199. )
  200. }
  201. private func startSaving() {
  202. guard mode == .settings else {
  203. self.continueSaving()
  204. return
  205. }
  206. authenticate(TherapySetting.insulinModel.authenticationChallengeDescription) {
  207. switch $0 {
  208. case .success: self.continueSaving()
  209. case .failure: break
  210. }
  211. }
  212. }
  213. private func continueSaving() {
  214. self.save(self.value)
  215. }
  216. var dismissButton: some View {
  217. Button(action: dismiss) {
  218. Text(LocalizedString("Close", comment: "Button text to close a modal"))
  219. }
  220. }
  221. }
  222. fileprivate extension HKUnit {
  223. /// An example value for the "ideal" target
  224. var glucoseExampleTargetValue: Double {
  225. if self == .milligramsPerDeciliter {
  226. return 100
  227. } else {
  228. return 5.5
  229. }
  230. }
  231. }