| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423 |
- import Combine
- import CoreData
- import Foundation
- extension Adjustments.StateModel {
- // MARK: - State Initialization and Updates
- /// Updates the latest Temp Target configuration for UI state and logic.
- /// First get the latest Temp Target corresponding NSManagedObjectID with a background fetch
- /// Then unpack it on the view context and update the State variables which can be used on in the View for some Logic
- /// This also needs to be called when we cancel an Temp Target via the Home View to update the State of the Button for this case
- func updateLatestTempTargetConfiguration() {
- Task {
- let id = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 1)
- async let updateState: () = updateLatestTempTargetConfigurationOfState(from: id)
- async let setTempTarget: () = setCurrentTempTarget(from: id)
- _ = await (updateState, setTempTarget)
- }
- }
- /// Updates state variables with the latest Temp Target configuration.
- @MainActor func updateLatestTempTargetConfigurationOfState(from IDs: [NSManagedObjectID]) async {
- do {
- let result = try IDs.compactMap { id in
- try viewContext.existingObject(with: id) as? TempTargetStored
- }
- isTempTargetEnabled = result.first?.enabled ?? false
- if !isOverrideEnabled {
- await resetTempTargetState()
- }
- } catch {
- debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update latest temp target configuration")
- }
- }
- /// Sets the current Temp Target for UI and logic purposes.
- @MainActor func setCurrentTempTarget(from IDs: [NSManagedObjectID]) async {
- do {
- guard let firstID = IDs.first else {
- activeTempTargetName = "Custom Temp Target"
- currentActiveTempTarget = nil
- return
- }
- if let tempTargetToEdit = try viewContext.existingObject(with: firstID) as? TempTargetStored {
- currentActiveTempTarget = tempTargetToEdit
- activeTempTargetName = tempTargetToEdit.name ?? "Custom Temp Target"
- tempTargetTarget = tempTargetToEdit.target?.decimalValue ?? 0
- }
- } catch {
- debugPrint(
- "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to set active preset name with error: \(error.localizedDescription)"
- )
- }
- }
- // MARK: - Temp Target Fetching and Setup
- /// Sets up Temp Targets using fetch and update functions.
- func setupTempTargets(
- fetchFunction: @escaping () async -> [NSManagedObjectID],
- updateFunction: @escaping @MainActor([TempTargetStored]) -> Void
- ) {
- Task {
- let ids = await fetchFunction()
- let tempTargetObjects = await fetchTempTargetObjects(for: ids)
- await updateFunction(tempTargetObjects)
- }
- }
- /// Fetches Temp Target objects from Core Data.
- @MainActor private func fetchTempTargetObjects(for IDs: [NSManagedObjectID]) async -> [TempTargetStored] {
- do {
- return try IDs.compactMap { id in
- try viewContext.existingObject(with: id) as? TempTargetStored
- }
- } catch {
- debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to fetch Temp Targets")
- return []
- }
- }
- /// Sets up the Temp Target presets array for the view.
- func setupTempTargetPresetsArray() {
- setupTempTargets(
- fetchFunction: tempTargetStorage.fetchForTempTargetPresets,
- updateFunction: { tempTargets in
- self.tempTargetPresets = tempTargets
- }
- )
- }
- /// Sets up the scheduled Temp Targets array for the view.
- func setupScheduledTempTargetsArray() {
- setupTempTargets(
- fetchFunction: tempTargetStorage.fetchScheduledTempTargets,
- updateFunction: { tempTargets in
- self.scheduledTempTargets = tempTargets
- }
- )
- }
- // MARK: - Temp Target Creation and Management
- /// Saves a Temp Target to storage.
- func saveTempTargetToStorage(tempTargets: [TempTarget]) {
- tempTargetStorage.saveTempTargetsToStorage(tempTargets)
- }
- /// Saves a Temp Target based on whether it is scheduled or custom.
- func invokeSaveOfCustomTempTargets() async {
- if date > Date() {
- await saveScheduledTempTarget()
- } else {
- await saveCustomTempTarget()
- }
- }
- /// Saves a scheduled Temp Target and activates it at the specified date.
- func saveScheduledTempTarget() async {
- let date = self.date
- guard date > Date() else { return }
- let tempTarget = TempTarget(
- name: tempTargetName,
- createdAt: date,
- targetTop: tempTargetTarget,
- targetBottom: tempTargetTarget,
- duration: tempTargetDuration,
- enteredBy: TempTarget.local,
- reason: TempTarget.custom,
- isPreset: false,
- enabled: false,
- halfBasalTarget: halfBasalTarget
- )
- await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
- setupScheduledTempTargetsArray()
- Task {
- await waitUntilDate(date)
- await disableAllActiveTempTargets(createTempTargetRunEntry: true)
- await enableScheduledTempTarget(for: date)
- tempTargetStorage.saveTempTargetsToStorage([tempTarget])
- }
- }
- /// Enables a scheduled Temp Target for a specific date.
- func enableScheduledTempTarget(for date: Date) async {
- let ids = await tempTargetStorage.fetchScheduledTempTarget(for: date)
- guard let firstID = ids.first else {
- debugPrint("No Temp Target found for the specified date.")
- return
- }
- await setCurrentTempTarget(from: ids)
- await MainActor.run {
- do {
- if let tempTarget = try viewContext.existingObject(with: firstID) as? TempTargetStored {
- tempTarget.enabled = true
- try viewContext.save()
- isTempTargetEnabled = true
- }
- } catch {
- debugPrint("Failed to enable the Temp Target: \(error.localizedDescription)")
- }
- }
- setupScheduledTempTargetsArray()
- }
- /// Waits until a target date before proceeding.
- private func waitUntilDate(_ targetDate: Date) async {
- while Date() < targetDate {
- let timeInterval = targetDate.timeIntervalSince(Date())
- let sleepDuration = min(timeInterval, 60.0)
- try? await Task.sleep(nanoseconds: UInt64(sleepDuration * 1_000_000_000))
- }
- }
- /// Saves a custom Temp Target and disables existing ones.
- func saveCustomTempTarget() async {
- await disableAllActiveTempTargets(createTempTargetRunEntry: true)
- let tempTarget = TempTarget(
- name: tempTargetName,
- /// We don't need to use the state var date here as we are using a different function for scheduled Temp Targets 'saveScheduledTempTarget()'
- createdAt: Date(),
- targetTop: tempTargetTarget,
- targetBottom: tempTargetTarget,
- duration: tempTargetDuration,
- enteredBy: TempTarget.local,
- reason: TempTarget.custom,
- isPreset: false,
- enabled: true,
- halfBasalTarget: halfBasalTarget
- )
- await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
- tempTargetStorage.saveTempTargetsToStorage([tempTarget])
- await resetTempTargetState()
- isTempTargetEnabled = true
- updateLatestTempTargetConfiguration()
- }
- /// Creates a new Temp Target preset.
- func saveTempTargetPreset() async {
- let tempTarget = TempTarget(
- name: tempTargetName,
- createdAt: Date(),
- targetTop: tempTargetTarget,
- targetBottom: tempTargetTarget,
- duration: tempTargetDuration,
- enteredBy: TempTarget.local,
- reason: TempTarget.custom,
- isPreset: true,
- enabled: false,
- halfBasalTarget: halfBasalTarget
- )
- await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
- await resetTempTargetState()
- setupTempTargetPresetsArray()
- }
- /// Enacts a Temp Target preset by enabling it.
- @MainActor func enactTempTargetPreset(withID id: NSManagedObjectID) async {
- do {
- let tempTargetToEnact = try viewContext.existingObject(with: id) as? TempTargetStored
- tempTargetToEnact?.enabled = true
- tempTargetToEnact?.date = Date()
- tempTargetToEnact?.isUploadedToNS = false
- isTempTargetEnabled = true
- async let disableTempTargets: () = disableAllActiveTempTargets(
- except: id,
- createTempTargetRunEntry: currentActiveTempTarget != nil
- )
- async let resetState: () = resetTempTargetState()
- _ = await (disableTempTargets, resetState)
- if viewContext.hasChanges {
- try viewContext.save()
- }
- updateLatestTempTargetConfiguration()
- let tempTarget = TempTarget(
- name: tempTargetToEnact?.name,
- createdAt: Date(),
- targetTop: tempTargetToEnact?.target?.decimalValue,
- targetBottom: tempTargetToEnact?.target?.decimalValue,
- duration: tempTargetToEnact?.duration?.decimalValue ?? 0,
- enteredBy: TempTarget.local,
- reason: TempTarget.custom,
- isPreset: true,
- enabled: true,
- halfBasalTarget: halfBasalTarget
- )
- tempTargetStorage.saveTempTargetsToStorage([tempTarget])
- } catch {
- debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact Override Preset")
- }
- }
- /// Disables all active Temp Targets.
- @MainActor func disableAllActiveTempTargets(except id: NSManagedObjectID? = nil, createTempTargetRunEntry: Bool) async {
- // Get ALL NSManagedObject IDs of ALL active Temp Targets to cancel every single Temp Target
- let ids = await tempTargetStorage.loadLatestTempTargetConfigurations(fetchLimit: 0) // 0 = no fetch limit
- await viewContext.perform {
- do {
- // Fetch the existing TempTargetStored objects from the context
- let results = try ids.compactMap { id in
- try self.viewContext.existingObject(with: id) as? TempTargetStored
- }
- // If there are no results, return early
- guard !results.isEmpty else { return }
- // Check if we also need to create a corresponding TempTargetRunStored entry, i.e. when the User uses the Cancel Button in Temp Target View
- if createTempTargetRunEntry {
- // Use the first temp target to create a new TempTargetRunStored entry
- if let canceledTempTarget = results.first {
- let newTempTargetRunStored = TempTargetRunStored(context: self.viewContext)
- newTempTargetRunStored.id = UUID()
- newTempTargetRunStored.name = canceledTempTarget.name
- newTempTargetRunStored.startDate = canceledTempTarget.date ?? .distantPast
- newTempTargetRunStored.endDate = Date()
- newTempTargetRunStored
- .target = canceledTempTarget.target ?? 0
- newTempTargetRunStored.tempTarget = canceledTempTarget
- newTempTargetRunStored.isUploadedToNS = false
- }
- }
- // Disable all temporary targets except the one with given id
- for tempTargetToCancel in results {
- if tempTargetToCancel.objectID != id {
- tempTargetToCancel.enabled = false
- }
- }
- // Save the context if there are changes
- if self.viewContext.hasChanges {
- try self.viewContext.save()
- // Update the storage
- self.tempTargetStorage.saveTempTargetsToStorage([TempTarget.cancel(at: Date().addingTimeInterval(-1))])
- }
- } catch {
- debugPrint(
- "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active TempTargets with error: \(error.localizedDescription)"
- )
- }
- }
- }
- /// Duplicates the current preset and cancels the previous one.
- @MainActor func duplicateTempTargetPresetAndCancelPreviousTempTarget() async {
- // We get the current active Preset by using currentActiveTempTarget which can either be a Preset or a custom Override
- guard let tempTargetPresetToDuplicate = currentActiveTempTarget,
- tempTargetPresetToDuplicate.isPreset == true else { return }
- // Copy the current TempTarget-Preset to not edit the underlying Preset
- let duplidateId = await tempTargetStorage.copyRunningTempTarget(tempTargetPresetToDuplicate)
- // Cancel the duplicated Temp Target
- // As we are on the Main Thread already we don't need to cancel via the objectID in this case
- do {
- try await viewContext.perform {
- tempTargetPresetToDuplicate.enabled = false
- guard self.viewContext.hasChanges else { return }
- try self.viewContext.save()
- }
- if let tempTargetToEdit = try viewContext.existingObject(with: duplidateId) as? TempTargetStored
- {
- currentActiveTempTarget = tempTargetToEdit
- activeTempTargetName = tempTargetToEdit.name ?? "Custom Temp Target"
- }
- } catch {
- debugPrint(
- "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel previous override with error: \(error.localizedDescription)"
- )
- }
- }
- /// Deletes a Temp Target preset.
- func invokeTempTargetPresetDeletion(_ objectID: NSManagedObjectID) async {
- await tempTargetStorage.deleteOverridePreset(objectID)
- setupTempTargetPresetsArray()
- }
- /// Resets Temp Target state variables.
- @MainActor func resetTempTargetState() async {
- tempTargetName = ""
- tempTargetTarget = 100
- tempTargetDuration = 0
- percentage = 100
- halfBasalTarget = settingHalfBasalTarget
- }
- // MARK: - Calculations
- /// Computes the half-basal target based on the current settings.
- func computeHalfBasalTarget(
- usingTarget initialTarget: Decimal? = nil,
- usingPercentage initialPercentage: Double? = nil
- ) -> Double {
- let adjustmentPercentage = initialPercentage ?? percentage
- let adjustmentRatio = Decimal(adjustmentPercentage / 100)
- let tempTargetValue: Decimal = initialTarget ?? tempTargetTarget
- var halfBasalTargetValue = halfBasalTarget
- if adjustmentRatio != 1 {
- halfBasalTargetValue = ((2 * adjustmentRatio * normalTarget) - normalTarget - (adjustmentRatio * tempTargetValue)) /
- (adjustmentRatio - 1)
- }
- return round(Double(halfBasalTargetValue))
- }
- /// Determines if sensitivity adjustment is enabled based on target.
- func isAdjustSensEnabled(usingTarget initialTarget: Decimal? = nil) -> Bool {
- let target = initialTarget ?? tempTargetTarget
- if target < normalTarget, lowTTlowersSens { return true }
- if target > normalTarget, highTTraisesSens || isExerciseModeActive { return true }
- return false
- }
- /// Computes the low value for the slider based on the target.
- func computeSliderLow(usingTarget initialTarget: Decimal? = nil) -> Double {
- let calcTarget = initialTarget ?? tempTargetTarget
- guard calcTarget != 0 else { return 15 } // oref defined maximum sensitivity
- let minSens = calcTarget < normalTarget ? 105 : 15
- return Double(max(0, minSens))
- }
- /// Computes the high value for the slider based on the target.
- func computeSliderHigh(usingTarget initialTarget: Decimal? = nil) -> Double {
- let calcTarget = initialTarget ?? tempTargetTarget
- guard calcTarget != 0 else { return Double(maxValue * 100) } // oref defined limit for increased insulin delivery
- let maxSens = calcTarget > normalTarget ? 95 : Double(maxValue * 100)
- return maxSens
- }
- /// Computes the adjusted percentage for the slider.
- func computeAdjustedPercentage(
- usingHBT initialHalfBasalTarget: Decimal? = nil,
- usingTarget initialTarget: Decimal? = nil
- ) -> Double {
- let halfBasalTargetValue = initialHalfBasalTarget ?? halfBasalTarget
- let calcTarget = initialTarget ?? tempTargetTarget
- let deviationFromNormal = halfBasalTargetValue - normalTarget
- let adjustmentFactor = deviationFromNormal + (calcTarget - normalTarget)
- let adjustmentRatio: Decimal = (deviationFromNormal * adjustmentFactor <= 0) ? maxValue : deviationFromNormal /
- adjustmentFactor
- return Double(min(adjustmentRatio, maxValue) * 100).rounded()
- }
- }
- enum TempTargetSensitivityAdjustmentType: String, CaseIterable {
- case standard = "Standard"
- case slider = "Custom"
- }
|