// // BasalRateScheduleEditor.swift // LoopKitUI // // Created by Michael Pangburn on 4/20/20. // Copyright © 2020 LoopKit Authors. All rights reserved. // import SwiftUI import HealthKit import LoopKit public typealias SyncBasalRateSchedule = (_ items: [RepeatingScheduleValue], _ completion: @escaping (Result) -> Void) -> Void public struct BasalRateScheduleEditor: View { var schedule: DailyQuantitySchedule? var supportedBasalRates: [Double] var guardrail: Guardrail var maximumScheduleEntryCount: Int var syncBasalRateSchedule: SyncBasalRateSchedule? var save: (BasalRateSchedule) -> Void let mode: SettingsPresentationMode @Environment(\.appName) private var appName /// - Precondition: `supportedBasalRates` is nonempty and sorted in ascending order. public init( schedule: BasalRateSchedule?, supportedBasalRates: [Double], maximumBasalRate: Double?, maximumScheduleEntryCount: Int, syncBasalRateSchedule: SyncBasalRateSchedule?, onSave save: @escaping (BasalRateSchedule) -> Void, mode: SettingsPresentationMode = .settings ) { self.schedule = schedule.map { schedule in DailyQuantitySchedule( unit: .internationalUnitsPerHour, dailyItems: schedule.items )! } if let maxBasal = maximumBasalRate { let partitioningIndex = supportedBasalRates.partitioningIndex(where: { $0 > maxBasal }) self.supportedBasalRates = Array(supportedBasalRates[.. Void)? = nil ) { self.init( schedule: therapySettingsViewModel.therapySettings.basalRateSchedule, supportedBasalRates: therapySettingsViewModel.pumpSupportedIncrements()?.basalRates ?? [], maximumBasalRate: therapySettingsViewModel.therapySettings.maximumBasalRatePerHour, maximumScheduleEntryCount: therapySettingsViewModel.pumpSupportedIncrements()?.maximumBasalScheduleEntryCount ?? 0, syncBasalRateSchedule: therapySettingsViewModel.syncBasalRateSchedule, onSave: { [weak therapySettingsViewModel] newBasalRates in therapySettingsViewModel?.saveBasalRates(basalRates: newBasalRates) didSave?() }, mode: mode ) } public var body: some View { QuantityScheduleEditor( title: Text(TherapySetting.basalRate.title), description: description, schedule: schedule, unit: .internationalUnitsPerHour, selectableValues: supportedBasalRates, guardrail: guardrail, quantitySelectionMode: .fractional, defaultFirstScheduleItemValue: guardrail.absoluteBounds.lowerBound, scheduleItemLimit: maximumScheduleEntryCount, confirmationAlertContent: confirmationAlertContent, guardrailWarning: { BasalRateGuardrailWarning( crossedThresholds: $0, isZeroUnitRateSelectable: self.supportedBasalRates.first! == 0 ) }, onSave: savingMechanism, mode: mode, settingType: .basalRate(maximumScheduleEntryCount) ) } private var description: Text { Text(TherapySetting.basalRate.descriptiveText(appName: appName)) } private var confirmationAlertContent: AlertContent { AlertContent( title: Text(LocalizedString("Save Basal Rates?", comment: "Alert title for confirming basal rates outside the recommended range")), message: Text(TherapySetting.basalRate.guardrailSaveWarningCaption) ) } private var savingMechanism: SavingMechanism> { switch mode { case .settings: return .asynchronous { quantitySchedule, completion in precondition(self.syncBasalRateSchedule != nil) self.syncBasalRateSchedule?(quantitySchedule.items) { result in switch result { case .success(let syncedSchedule): DispatchQueue.main.async { self.save(syncedSchedule) } completion(nil) case .failure(let error): completion(error) } } } case .acceptanceFlow: // TODO: get timezone from pump return .synchronous { quantitySchedule in let schedule = BasalRateSchedule(dailyItems: quantitySchedule.items, timeZone: .currentFixed)! self.save(schedule) } } } } private struct BasalRateGuardrailWarning: View { var crossedThresholds: [SafetyClassification.Threshold] var isZeroUnitRateSelectable: Bool var body: some View { assert(!crossedThresholds.isEmpty) let caption = self.isZeroUnitRateSelectable && crossedThresholds.allSatisfy({ $0 == .minimum }) ? Text(LocalizedString("A value of 0 U/hr means you will be scheduled to receive no basal insulin.", comment: "Warning text for basal rate of 0 U/hr")) : nil return GuardrailWarning( therapySetting: .basalRate, title: crossedThresholds.count == 1 ? singularWarningTitle(for: crossedThresholds.first!) : multipleWarningTitle, thresholds: crossedThresholds, caption: caption ) } private func singularWarningTitle(for threshold: SafetyClassification.Threshold) -> Text { switch threshold { case .minimum where isZeroUnitRateSelectable: return Text(LocalizedString("No Basal Insulin", comment: "Title text for the zero basal rate warning")) case .minimum, .belowRecommended: return Text(LocalizedString("Low Basal Rate", comment: "Title text for the low basal rate warning")) case .aboveRecommended, .maximum: return Text(LocalizedString("High Basal Rate", comment: "Title text for the high basal rate warning")) } } private var multipleWarningTitle: Text { isZeroUnitRateSelectable && crossedThresholds.allSatisfy({ $0 == .minimum }) ? Text(LocalizedString("No Basal Insulin", comment: "Title text for the zero basal rate warning")) : Text(LocalizedString("Basal Rates", comment: "Title text for multi-value basal rate warning")) } }