| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- //
- // InsulinModelSelection.swift
- // Loop
- //
- // Created by Michael Pangburn on 7/14/20.
- // Copyright © 2020 LoopKit Authors. All rights reserved.
- //
- import HealthKit
- import SwiftUI
- import LoopKit
- public struct InsulinModelSelection: View {
- @EnvironmentObject private var displayGlucoseUnitObservable: DisplayGlucoseUnitObservable
- @Environment(\.appName) private var appName
- @Environment(\.dismissAction) private var dismiss
- @Environment(\.authenticate) private var authenticate
- @State private var value: ExponentialInsulinModelPreset
- @State private var chartManager: ChartsManager
- private let initialValue: ExponentialInsulinModelPreset
- private let insulinSensitivitySchedule: InsulinSensitivitySchedule
- private let mode: SettingsPresentationMode
- private let save: (_ insulinModelPreset: ExponentialInsulinModelPreset) -> Void
- static let defaultInsulinSensitivitySchedule = InsulinSensitivitySchedule(unit: .milligramsPerDeciliter, dailyItems: [RepeatingScheduleValue<Double>(startTime: 0, value: 40)])!
-
- private var displayGlucoseUnit: HKUnit {
- displayGlucoseUnitObservable.displayGlucoseUnit
- }
-
- public init(
- value: ExponentialInsulinModelPreset,
- insulinSensitivitySchedule: InsulinSensitivitySchedule?,
- chartColors: ChartColorPalette,
- onSave save: @escaping (_ insulinModelPreset: ExponentialInsulinModelPreset) -> Void,
- mode: SettingsPresentationMode
- ){
- self._value = State(initialValue: value)
- self.initialValue = value
- self.insulinSensitivitySchedule = insulinSensitivitySchedule ?? Self.defaultInsulinSensitivitySchedule
- self.save = save
- self.mode = mode
- let chartManager = ChartsManager(
- colors: chartColors,
- settings: .default,
- axisLabelFont: .systemFont(ofSize: 12),
- charts: [InsulinModelChart()],
- traitCollection: .current
- )
- chartManager.startDate = Calendar.current.nextDate(
- after: Date(),
- matching: DateComponents(minute: 0),
- matchingPolicy: .strict,
- direction: .backward
- ) ?? Date()
- self._chartManager = State(initialValue: chartManager)
- }
- public init(
- mode: SettingsPresentationMode,
- therapySettingsViewModel: TherapySettingsViewModel,
- chartColors: ChartColorPalette,
- didSave: (() -> Void)? = nil
- ) {
- self.init(
- value: therapySettingsViewModel.therapySettings.defaultRapidActingModel ?? .rapidActingAdult,
- insulinSensitivitySchedule: therapySettingsViewModel.therapySettings.insulinSensitivitySchedule,
- chartColors: chartColors,
- onSave: { [weak therapySettingsViewModel] insulinModelPreset in
- therapySettingsViewModel?.saveInsulinModel(insulinModelPreset: insulinModelPreset)
- didSave?()
- },
- mode: mode
- )
- }
- public var body: some View {
- switch mode {
- case .acceptanceFlow:
- content
- case .settings:
- contentWithCancel
- .navigationBarTitleDisplayMode(.large)
- }
- }
-
- private var contentWithCancel: some View {
- if value == initialValue {
- return AnyView(content
- .navigationBarBackButtonHidden(false)
- .navigationBarItems(leading: EmptyView())
- )
- } else {
- return AnyView(content
- .navigationBarBackButtonHidden(true)
- .navigationBarItems(leading: cancelButton)
- )
- }
- }
-
- private var cancelButton: some View {
- Button(action: { dismiss() } ) { Text(LocalizedString("Cancel", comment: "Cancel editing settings button title")) }
- }
-
- private var content: some View {
- VStack(spacing: 0) {
- CardList(title: Text(LocalizedString("Insulin Model", comment: "Title text for insulin model")),
- style: .simple(CardStack(cards: [card])))
- Button(action: { startSaving() }) {
- Text(mode.buttonText())
- .actionButtonStyle(.primary)
- .padding()
- }
- .disabled(value == initialValue && mode != .acceptanceFlow)
- // Styling to mimic the floating button of a ConfigurationPage
- .padding(.bottom)
- .background(Color(.secondarySystemGroupedBackground).shadow(radius: 5))
- }
- .supportedInterfaceOrientations(.portrait)
- .edgesIgnoringSafeArea(.bottom)
- }
-
- private var card: Card {
- Card {
- Section {
- SettingDescription(
- text: insulinModelSettingDescription,
- informationalContent: {
- TherapySetting.insulinModel.helpScreen()
- }
- )
- .padding(4)
- .padding(.top, 4)
- VStack {
- InsulinModelChartView(
- chartManager: chartManager,
- glucoseUnit: displayGlucoseUnit,
- selectedInsulinModelValues: selectedInsulinModelValues,
- unselectedInsulinModelValues: unselectedInsulinModelValues,
- glucoseDisplayRange: endingGlucoseQuantity...startingGlucoseQuantity
- )
- .frame(height: 170)
- CheckmarkListItem(
- title: Text(ExponentialInsulinModelPreset.rapidActingAdult.title),
- description: Text(ExponentialInsulinModelPreset.rapidActingAdult.subtitle),
- isSelected: isSelected(ExponentialInsulinModelPreset.rapidActingAdult)
- )
- .padding(.vertical, 4)
- .contentShape(Rectangle())
- }
- SectionDivider()
- CheckmarkListItem(
- title: Text(ExponentialInsulinModelPreset.rapidActingChild.title),
- description: Text(ExponentialInsulinModelPreset.rapidActingChild.subtitle),
- isSelected: isSelected(ExponentialInsulinModelPreset.rapidActingChild)
- )
- .padding(.vertical, 4)
- .padding(.bottom, 4)
- }
- .buttonStyle(PlainButtonStyle()) // Disable row highlighting on selection
- }
- }
- var insulinModelSettingDescription: Text {
- let spellOutFormatter = NumberFormatter()
- spellOutFormatter.numberStyle = .spellOut
- let modelCountString = spellOutFormatter.string(from: selectableInsulinModelSettings.count as NSNumber)!
- 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))
- }
- var selectableInsulinModelSettings: [ExponentialInsulinModelPreset] {
- return [
- .rapidActingAdult,
- .rapidActingChild
- ]
- }
- private var selectedInsulinModelValues: [GlucoseValue] {
- oneUnitBolusEffectPrediction(using: value)
- }
- private var unselectedInsulinModelValues: [[GlucoseValue]] {
- selectableInsulinModelSettings
- .filter { $0 != value }
- .map { oneUnitBolusEffectPrediction(using: $0) }
- }
-
- private func oneUnitBolusEffectPrediction(using modelPreset: ExponentialInsulinModelPreset) -> [GlucoseValue] {
- let bolus = DoseEntry(type: .bolus, startDate: chartManager.startDate, value: 1, unit: .units, insulinType: .novolog)
- let startingGlucoseSample = HKQuantitySample(type: HKQuantityType.quantityType(forIdentifier: .bloodGlucose)!, quantity: startingGlucoseQuantity, start: chartManager.startDate, end: chartManager.startDate)
- let effects = [bolus].glucoseEffects(insulinModelProvider: StaticInsulinModelProvider(modelPreset), longestEffectDuration: .hours(6), insulinSensitivity: insulinSensitivitySchedule)
- return LoopMath.predictGlucose(startingAt: startingGlucoseSample, effects: effects)
- }
- private var startingGlucoseQuantity: HKQuantity {
- let startingGlucoseValue = insulinSensitivitySchedule.quantity(at: chartManager.startDate).doubleValue(for: displayGlucoseUnit) + displayGlucoseUnit.glucoseExampleTargetValue
- return HKQuantity(unit: displayGlucoseUnit, doubleValue: startingGlucoseValue)
- }
- private var endingGlucoseQuantity: HKQuantity {
- HKQuantity(unit: displayGlucoseUnit, doubleValue: displayGlucoseUnit.glucoseExampleTargetValue)
- }
- private func isSelected(_ preset: ExponentialInsulinModelPreset) -> Binding<Bool> {
- Binding(
- get: { value == preset },
- set: { isSelected in
- if isSelected {
- withAnimation {
- value = preset
- }
- }
- }
- )
- }
- private func startSaving() {
- guard mode == .settings else {
- continueSaving()
- return
- }
- authenticate(TherapySetting.insulinModel.authenticationChallengeDescription) {
- switch $0 {
- case .success: continueSaving()
- case .failure: break
- }
- }
- }
-
- private func continueSaving() {
- save(value)
- }
- var dismissButton: some View {
- Button(action: dismiss) {
- Text(LocalizedString("Close", comment: "Button text to close a modal"))
- }
- }
- }
- fileprivate extension HKUnit {
- /// An example value for the "ideal" target
- var glucoseExampleTargetValue: Double {
- if self == .milligramsPerDeciliter {
- return 100
- } else {
- return 5.5
- }
- }
- }
- fileprivate struct SectionDivider: View {
- var body: some View {
- Divider()
- .padding(.trailing, -16)
- }
- }
|