InsulinModelSelection.swift 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  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. @EnvironmentObject private var displayGlucosePreference: DisplayGlucosePreference
  13. @Environment(\.appName) private var appName
  14. @Environment(\.dismissAction) private var dismiss
  15. @Environment(\.authenticate) private var authenticate
  16. @State private var value: ExponentialInsulinModelPreset
  17. @State private var chartManager: ChartsManager
  18. private let initialValue: ExponentialInsulinModelPreset
  19. private let insulinSensitivitySchedule: InsulinSensitivitySchedule
  20. private let mode: SettingsPresentationMode
  21. private let save: (_ insulinModelPreset: ExponentialInsulinModelPreset) -> Void
  22. static let defaultInsulinSensitivitySchedule = InsulinSensitivitySchedule(unit: .milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue<Double>(startTime: 0, value: 40)])!
  23. private var displayGlucoseUnit: HKUnit {
  24. displayGlucosePreference.unit
  25. }
  26. public init(
  27. value: ExponentialInsulinModelPreset,
  28. insulinSensitivitySchedule: InsulinSensitivitySchedule?,
  29. chartColors: ChartColorPalette,
  30. onSave save: @escaping (_ insulinModelPreset: ExponentialInsulinModelPreset) -> 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.mode = mode
  38. let chartManager = ChartsManager(
  39. colors: chartColors,
  40. settings: .default,
  41. axisLabelFont: .systemFont(ofSize: 12),
  42. charts: [InsulinModelChart()],
  43. traitCollection: .current
  44. )
  45. chartManager.startDate = Calendar.current.nextDate(
  46. after: Date(),
  47. matching: DateComponents(minute: 0),
  48. matchingPolicy: .strict,
  49. direction: .backward
  50. ) ?? Date()
  51. self._chartManager = State(initialValue: chartManager)
  52. }
  53. public init(
  54. mode: SettingsPresentationMode,
  55. therapySettingsViewModel: TherapySettingsViewModel,
  56. chartColors: ChartColorPalette,
  57. didSave: (() -> Void)? = nil
  58. ) {
  59. self.init(
  60. value: therapySettingsViewModel.therapySettings.defaultRapidActingModel ?? .rapidActingAdult,
  61. insulinSensitivitySchedule: therapySettingsViewModel.therapySettings.insulinSensitivitySchedule,
  62. chartColors: chartColors,
  63. onSave: { [weak therapySettingsViewModel] insulinModelPreset in
  64. therapySettingsViewModel?.saveInsulinModel(insulinModelPreset: insulinModelPreset)
  65. didSave?()
  66. },
  67. mode: mode
  68. )
  69. }
  70. public var body: some View {
  71. switch mode {
  72. case .acceptanceFlow:
  73. content
  74. case .settings:
  75. contentWithCancel
  76. .navigationBarTitleDisplayMode(.inline)
  77. }
  78. }
  79. private var contentWithCancel: some View {
  80. content
  81. .navigationBarBackButtonHidden(value != initialValue)
  82. .toolbar {
  83. ToolbarItem(placement: .navigationBarLeading) {
  84. leadingNavigationBarItem
  85. }
  86. }
  87. }
  88. @ViewBuilder
  89. private var leadingNavigationBarItem: some View {
  90. if value != initialValue {
  91. cancelButton
  92. } else {
  93. EmptyView()
  94. }
  95. }
  96. private var cancelButton: some View {
  97. Button(action: { dismiss() } ) { Text(LocalizedString("Cancel", comment: "Cancel editing settings button title")) }
  98. }
  99. private var content: some View {
  100. VStack(spacing: 0) {
  101. CardList(title: Text(LocalizedString("Insulin Model", comment: "Title text for insulin model")),
  102. style: .simple(CardStack(cards: [card])))
  103. Button(action: { startSaving() }) {
  104. Text(mode.buttonText())
  105. .actionButtonStyle(.primary)
  106. .padding()
  107. }
  108. .disabled(value == initialValue && mode != .acceptanceFlow)
  109. // Styling to mimic the floating button of a ConfigurationPage
  110. .padding(.bottom)
  111. .background(Color(.secondarySystemGroupedBackground).shadow(radius: 5))
  112. }
  113. .supportedInterfaceOrientations(.portrait)
  114. .edgesIgnoringSafeArea(.bottom)
  115. }
  116. private var card: Card {
  117. Card {
  118. Section {
  119. SettingDescription(
  120. text: insulinModelSettingDescription,
  121. informationalContent: {
  122. TherapySetting.insulinModel.helpScreen()
  123. }
  124. )
  125. .padding(4)
  126. .padding(.top, 4)
  127. VStack {
  128. InsulinModelChartView(
  129. chartManager: chartManager,
  130. glucoseUnit: displayGlucoseUnit,
  131. selectedInsulinModelValues: selectedInsulinModelValues,
  132. unselectedInsulinModelValues: unselectedInsulinModelValues,
  133. glucoseDisplayRange: endingGlucoseQuantity...startingGlucoseQuantity
  134. )
  135. .frame(height: 170)
  136. CheckmarkListItem(
  137. title: Text(ExponentialInsulinModelPreset.rapidActingAdult.title),
  138. description: Text(ExponentialInsulinModelPreset.rapidActingAdult.subtitle),
  139. isSelected: isSelected(ExponentialInsulinModelPreset.rapidActingAdult)
  140. )
  141. .padding(.vertical, 4)
  142. .contentShape(Rectangle())
  143. }
  144. SectionDivider()
  145. CheckmarkListItem(
  146. title: Text(ExponentialInsulinModelPreset.rapidActingChild.title),
  147. description: Text(ExponentialInsulinModelPreset.rapidActingChild.subtitle),
  148. isSelected: isSelected(ExponentialInsulinModelPreset.rapidActingChild)
  149. )
  150. .padding(.vertical, 4)
  151. .padding(.bottom, 4)
  152. }
  153. .buttonStyle(PlainButtonStyle()) // Disable row highlighting on selection
  154. }
  155. }
  156. var insulinModelSettingDescription: Text {
  157. let spellOutFormatter = NumberFormatter()
  158. spellOutFormatter.numberStyle = .spellOut
  159. let modelCountString = spellOutFormatter.string(from: selectableInsulinModelSettings.count as NSNumber)!
  160. 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))
  161. }
  162. var selectableInsulinModelSettings: [ExponentialInsulinModelPreset] {
  163. return [
  164. .rapidActingAdult,
  165. .rapidActingChild
  166. ]
  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 modelPreset: ExponentialInsulinModelPreset) -> [GlucoseValue] {
  177. let bolus = DoseEntry(type: .bolus, startDate: chartManager.startDate, value: 1, unit: .units, insulinType: .novolog)
  178. let startingGlucoseSample = HKQuantitySample(type: HealthKitSampleStore.glucoseType, quantity: startingGlucoseQuantity, start: chartManager.startDate, end: chartManager.startDate)
  179. let effects = [bolus].glucoseEffects(insulinModelProvider: StaticInsulinModelProvider(modelPreset), longestEffectDuration: .hours(6), 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: displayGlucoseUnit) + displayGlucoseUnit.glucoseExampleTargetValue
  184. return HKQuantity(unit: displayGlucoseUnit, doubleValue: startingGlucoseValue)
  185. }
  186. private var endingGlucoseQuantity: HKQuantity {
  187. HKQuantity(unit: displayGlucoseUnit, doubleValue: displayGlucoseUnit.glucoseExampleTargetValue)
  188. }
  189. private func isSelected(_ preset: ExponentialInsulinModelPreset) -> Binding<Bool> {
  190. Binding(
  191. get: { value == preset },
  192. set: { isSelected in
  193. if isSelected {
  194. withAnimation {
  195. value = preset
  196. }
  197. }
  198. }
  199. )
  200. }
  201. private func startSaving() {
  202. guard mode == .settings else {
  203. continueSaving()
  204. return
  205. }
  206. authenticate(TherapySetting.insulinModel.authenticationChallengeDescription) {
  207. switch $0 {
  208. case .success: continueSaving()
  209. case .failure: break
  210. }
  211. }
  212. }
  213. private func continueSaving() {
  214. save(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. }
  232. fileprivate struct SectionDivider: View {
  233. var body: some View {
  234. Divider()
  235. .padding(.trailing, -16)
  236. }
  237. }