Quellcode durchsuchen

Reorder cont'd; add comments and docstrings

Deniz Cengiz vor 1 Jahr
Ursprung
Commit
172a781583

+ 4 - 0
FreeAPS.xcodeproj/project.pbxproj

@@ -460,6 +460,7 @@
 		DD5DC9F32CF3D9DD00AB8703 /* AdjustmentsStateModel+TempTargets.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F22CF3D9D600AB8703 /* AdjustmentsStateModel+TempTargets.swift */; };
 		DD5DC9F72CF3DA9300AB8703 /* TargetPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F62CF3DA9300AB8703 /* TargetPicker.swift */; };
 		DD5DC9F92CF3DAA900AB8703 /* RadioButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9F82CF3DAA900AB8703 /* RadioButton.swift */; };
+		DD5DC9FB2CF3E1B100AB8703 /* AdjustmentsStateModel+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD5DC9FA2CF3E1AA00AB8703 /* AdjustmentsStateModel+Helpers.swift */; };
 		DD68889D2C386E17006E3C44 /* NightscoutExercise.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */; };
 		DD6B7CB22C7B6F0800B75029 /* Rounding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB12C7B6F0800B75029 /* Rounding.swift */; };
 		DD6B7CB42C7B71F700B75029 /* ForecastDisplayType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */; };
@@ -1147,6 +1148,7 @@
 		DD5DC9F22CF3D9D600AB8703 /* AdjustmentsStateModel+TempTargets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsStateModel+TempTargets.swift"; sourceTree = "<group>"; };
 		DD5DC9F62CF3DA9300AB8703 /* TargetPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetPicker.swift; sourceTree = "<group>"; };
 		DD5DC9F82CF3DAA900AB8703 /* RadioButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RadioButton.swift; sourceTree = "<group>"; };
+		DD5DC9FA2CF3E1AA00AB8703 /* AdjustmentsStateModel+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AdjustmentsStateModel+Helpers.swift"; sourceTree = "<group>"; };
 		DD68889C2C386E17006E3C44 /* NightscoutExercise.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NightscoutExercise.swift; sourceTree = "<group>"; };
 		DD6B7CB12C7B6F0800B75029 /* Rounding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Rounding.swift; sourceTree = "<group>"; };
 		DD6B7CB32C7B71F700B75029 /* ForecastDisplayType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastDisplayType.swift; sourceTree = "<group>"; };
@@ -2770,6 +2772,7 @@
 		DD5DC9EF2CF3D95400AB8703 /* AdjustmentsStateModel+Extensions */ = {
 			isa = PBXGroup;
 			children = (
+				DD5DC9FA2CF3E1AA00AB8703 /* AdjustmentsStateModel+Helpers.swift */,
 				DD5DC9F22CF3D9D600AB8703 /* AdjustmentsStateModel+TempTargets.swift */,
 				DD5DC9F02CF3D96E00AB8703 /* AdjustmentsStateModel+Overrides.swift */,
 			);
@@ -3746,6 +3749,7 @@
 				BD7DA9A52AE06DFC00601B20 /* BolusCalculatorConfigDataFlow.swift in Sources */,
 				6EADD581738D64431902AC0A /* (null) in Sources */,
 				CE94598729E9E4110047C9C6 /* WatchConfigRootView.swift in Sources */,
+				DD5DC9FB2CF3E1B100AB8703 /* AdjustmentsStateModel+Helpers.swift in Sources */,
 				DDF847E42C5C288F0049BB3B /* LiveActivitySettingsRootView.swift in Sources */,
 				DD88C8E22C50420800F2D558 /* DefinitionRow.swift in Sources */,
 				B7C465E9472624D8A2BE2A6A /* (null) in Sources */,

+ 93 - 0
FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Helpers.swift

@@ -0,0 +1,93 @@
+import SwiftUI
+
+extension Adjustments.StateModel {
+    /// Returns a description of how insulin doses are adjusted based on percentage.
+    func percentageDescription(_ percent: Double) -> Text? {
+        if percent.isNaN || percent == 100 { return nil }
+
+        var description: String = "Insulin doses will be "
+
+        if percent < 100 {
+            description += "decreased by "
+        } else {
+            description += "increased by "
+        }
+
+        let deviationFrom100 = abs(percent - 100)
+        description += String(format: "%.0f% %.", deviationFrom100)
+
+        return Text(description)
+    }
+
+    /// Checks if the device is using a 24-hour time format.
+    func is24HourFormat() -> Bool {
+        let formatter = DateFormatter()
+        formatter.locale = Locale.current
+        formatter.dateStyle = .none
+        formatter.timeStyle = .short
+        let dateString = formatter.string(from: Date())
+
+        return !dateString.contains("AM") && !dateString.contains("PM")
+    }
+
+    /// Converts a given hour to a 12-hour AM/PM format string.
+    func convertTo12HourFormat(_ hour: Int) -> String {
+        let formatter = DateFormatter()
+        formatter.dateFormat = "h a"
+
+        let calendar = Calendar.current
+        let components = DateComponents(hour: hour)
+        let date = calendar.date(from: components) ?? Date()
+
+        return formatter.string(from: date)
+    }
+
+    /// Formats a given 24-hour time number as a two-digit string.
+    func format24Hour(_ hour: Int) -> String {
+        String(format: "%02d", hour)
+    }
+
+    /// Converts a duration in minutes to a formatted string (e.g., "1 hr 30 min").
+    func formatHrMin(_ durationInMinutes: Int) -> String {
+        let hours = durationInMinutes / 60
+        let minutes = durationInMinutes % 60
+
+        switch (hours, minutes) {
+        case let (0, m):
+            return "\(m) min"
+        case let (h, 0):
+            return "\(h) hr"
+        default:
+            return "\(hours) hr \(minutes) min"
+        }
+    }
+
+    /// Converts hours and minutes to total minutes as a `Decimal`.
+    func convertToMinutes(_ hours: Int, _ minutes: Int) -> Decimal {
+        let totalMinutes = (hours * 60) + minutes
+        return Decimal(max(0, totalMinutes))
+    }
+}
+
+extension PickerSettingsProvider {
+    /// Generates picker values based on a setting, optionally rounding minimum to the nearest step.
+    func generatePickerValues(from setting: PickerSetting, units: GlucoseUnits, roundMinToStep: Bool) -> [Decimal] {
+        if !roundMinToStep {
+            return generatePickerValues(from: setting, units: units)
+        }
+
+        // Adjust min to be divisible by step
+        var newSetting = setting
+        var min = Double(newSetting.min)
+        let step = Double(newSetting.step)
+        let remainder = min.truncatingRemainder(dividingBy: step)
+        if remainder != 0 {
+            // Move min up to the next value divisible by targetStep
+            min += (step - remainder)
+        }
+
+        newSetting.min = Decimal(min)
+
+        return generatePickerValues(from: newSetting, units: units)
+    }
+}

+ 36 - 62
FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+Overrides.swift

@@ -5,48 +5,40 @@ import Foundation
 extension Adjustments.StateModel {
     // MARK: - Enact Overrides
 
-    /// here we only have to update the Boolean Flag 'enabled'
+    /// Enacts an Override Preset by enabling it and disabling others.
     @MainActor func enactOverridePreset(withID id: NSManagedObjectID) async {
         do {
-            /// get the underlying NSManagedObject of the Override that should be enabled
             let overrideToEnact = try viewContext.existingObject(with: id) as? OverrideStored
             overrideToEnact?.enabled = true
             overrideToEnact?.date = Date()
             overrideToEnact?.isUploadedToNS = false
-
-            /// Update the 'Cancel Override' button state
             isEnabled = true
 
-            /// disable all active Overrides and reset state variables
-            /// do not create a OverrideRunEntry because we only want that if we cancel a running Override, not when enacting a Preset
             await disableAllActiveOverrides(except: id, createOverrideRunEntry: currentActiveOverride != nil)
-
             await resetStateVariables()
 
             guard viewContext.hasChanges else { return }
             try viewContext.save()
 
-            // Update View
             updateLatestOverrideConfiguration()
         } catch {
             debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact Override Preset")
         }
     }
 
-    // MARK: - Save the Override that we want to cancel to the OverrideRunStored Entity, then cancel ALL active overrides
+    // MARK: - Disable Overrides
 
+    /// Disables all active Overrides, optionally creating a run entry.
     @MainActor func disableAllActiveOverrides(except overrideID: NSManagedObjectID? = nil, createOverrideRunEntry: Bool) async {
         // Get ALL NSManagedObject IDs of ALL active Override to cancel every single Override
-        let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0) // 0 = no fetch limit
-
+        let ids = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 0)
+        
         await viewContext.perform {
             do {
                 // Fetch the existing OverrideStored objects from the context
                 let results = try ids.compactMap { id in
                     try self.viewContext.existingObject(with: id) as? OverrideStored
                 }
-
-                // If there are no results, return early
                 guard !results.isEmpty else { return }
 
                 // Check if we also need to create a corresponding OverrideRunStored entry, i.e. when the User uses the Cancel Button in Override View
@@ -65,32 +57,27 @@ extension Adjustments.StateModel {
                     }
                 }
 
-                // Disable all override except the one with overrideID
-                for overrideToCancel in results {
-                    if overrideToCancel.objectID != overrideID {
-                        overrideToCancel.enabled = false
-                    }
+                // Disable all overrides except the one with overrideID
+                for overrideToCancel in results where overrideToCancel.objectID != overrideID {
+                    overrideToCancel.enabled = false
                 }
 
-                // Save the context if there are changes
                 if self.viewContext.hasChanges {
+                    // Save changes and update the View
                     try self.viewContext.save()
-
-                    // Update the View
                     self.updateLatestOverrideConfiguration()
                 }
             } catch {
                 debugPrint(
-                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides with error: \(error.localizedDescription)"
+                    "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to disable active Overrides: \(error.localizedDescription)"
                 )
             }
         }
     }
 
-    // MARK: - Override (presets) save operations
+    // MARK: - Save Overrides
 
-    // Saves a Custom Override in a background context
-    /// not a Preset
+    /// Saves a custom Override and activates it.
     func saveCustomOverride() async {
         let override = Override(
             name: overrideName,
@@ -128,8 +115,9 @@ extension Adjustments.StateModel {
         updateLatestOverrideConfiguration()
     }
 
-    // Save Presets
-    /// enabled has to be false, isPreset has to be true
+    /// Saves an Override Preset without activating it.
+    /// `enabled` has to be false
+    /// `isPreset` has to be true
     func saveOverridePreset() async {
         let preset = Override(
             name: overrideName,
@@ -156,25 +144,22 @@ extension Adjustments.StateModel {
 
         async let storeOverride: () = overrideStorage.storeOverride(override: preset)
         async let resetState: () = resetStateVariables()
-
         _ = await (storeOverride, resetState)
-
-        // Update Presets View
         setupOverridePresetsArray()
-
         await nightscoutManager.uploadProfiles()
     }
 
-    // MARK: - Setup Override Presets Array
+    // MARK: - Override Preset Management
 
-    // Fill the array of the Override Presets to display them in the UI
+    /// Sets up the array of Override Presets for UI display.
     func setupOverridePresetsArray() {
         Task {
-            let ids = await self.overrideStorage.fetchForOverridePresets()
+            let ids = await overrideStorage.fetchForOverridePresets()
             await updateOverridePresetsArray(with: ids)
         }
     }
 
+    /// Updates the array of Override Presets from Core Data.
     @MainActor private func updateOverridePresetsArray(with IDs: [NSManagedObjectID]) async {
         do {
             let overrideObjects = try IDs.compactMap { id in
@@ -183,24 +168,21 @@ extension Adjustments.StateModel {
             overridePresets = overrideObjects
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to extract Overrides as NSManagedObjects from the NSManagedObjectIDs with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to extract Overrides: \(error.localizedDescription)"
             )
         }
     }
 
-    // MARK: - Override Preset Deletion
-
+    /// Deletes an Override Preset and updates the view.
     func invokeOverridePresetDeletion(_ objectID: NSManagedObjectID) async {
         await overrideStorage.deleteOverridePreset(objectID)
-
-        // Update Presets View
         setupOverridePresetsArray()
-
         await nightscoutManager.uploadProfiles()
     }
 
-    // MARK: - Setup the State variables with the last Override configuration
+    // MARK: - Update Latest Override Configuration
 
+    /// Updates the latest Override configuration and state.
     /// First get the latest Overrides 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 Override via the Home View to update the State of the Button for this case
@@ -209,29 +191,26 @@ extension Adjustments.StateModel {
             let id = await overrideStorage.loadLatestOverrideConfigurations(fetchLimit: 1)
             async let updateState: () = updateLatestOverrideConfigurationOfState(from: id)
             async let setOverride: () = setCurrentOverride(from: id)
-
             _ = await (updateState, setOverride)
         }
     }
 
+    /// Updates state variables with the latest Override configuration.
     @MainActor func updateLatestOverrideConfigurationOfState(from IDs: [NSManagedObjectID]) async {
         do {
             let result = try IDs.compactMap { id in
                 try viewContext.existingObject(with: id) as? OverrideStored
             }
             isEnabled = result.first?.enabled ?? false
-
             if !isEnabled {
                 await resetStateVariables()
             }
         } catch {
-            debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to updateLatestOverrideConfiguration"
-            )
+            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update latest Override configuration")
         }
     }
 
-    // Sets the current active Preset name to show in the UI
+    /// Sets the current active Override for UI purposes.
     @MainActor func setCurrentOverride(from IDs: [NSManagedObjectID]) async {
         do {
             guard let firstID = IDs.first else {
@@ -246,47 +225,40 @@ extension Adjustments.StateModel {
             }
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to set active preset name with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to set active Override: \(error.localizedDescription)"
             )
         }
     }
 
+    /// Duplicates the active Override Preset and cancels the previous one.
     @MainActor func duplicateOverridePresetAndCancelPreviousOverride() async {
-        // We get the current active Preset by using currentActiveOverride which can either be a Preset or a custom Override
-        guard let overridePresetToDuplicate = currentActiveOverride, overridePresetToDuplicate.isPreset == true else { return }
+        guard let overridePresetToDuplicate = currentActiveOverride, overridePresetToDuplicate.isPreset else { return }
 
-        // Copy the current Override-Preset to not edit the underlying Preset
-        let duplidateId = await overrideStorage.copyRunningOverride(overridePresetToDuplicate)
+        let duplicateId = await overrideStorage.copyRunningOverride(overridePresetToDuplicate)
 
-        // Cancel the duplicated Override
-        /// 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 {
                 overridePresetToDuplicate.enabled = false
-
                 guard self.viewContext.hasChanges else { return }
                 try self.viewContext.save()
             }
 
-            // Update View
-            // TODO: -
-            if let overrideToEdit = try viewContext.existingObject(with: duplidateId) as? OverrideStored
-            {
+            if let overrideToEdit = try viewContext.existingObject(with: duplicateId) as? OverrideStored {
                 currentActiveOverride = overrideToEdit
                 activeOverrideName = overrideToEdit.name ?? "Custom Override"
             }
         } catch {
             debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel previous override with error: \(error.localizedDescription)"
+                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to cancel previous Override: \(error.localizedDescription)"
             )
         }
     }
 
-    // MARK: - Helper functions for Overrides
+    // MARK: - Helper Functions
 
+    /// Resets state variables to default values.
     @MainActor func resetStateVariables() async {
         id = ""
-
         overrideDuration = 0
         indefinite = true
         overridePercentage = 100
@@ -305,6 +277,7 @@ extension Adjustments.StateModel {
         target = currentGlucoseTarget
     }
 
+    /// Rounds a target value to the nearest step.
     static func roundTargetToStep(_ target: Decimal, _ step: Decimal) -> Decimal {
         // Convert target and step to NSDecimalNumber
         guard let targetValue = NSDecimalNumber(decimal: target).doubleValue as Double?,
@@ -326,6 +299,7 @@ extension Adjustments.StateModel {
         return target
     }
 
+    /// Rounds an Override percentage to the nearest step.
     static func roundOverridePercentageToStep(_ percentage: Double, _ step: Int) -> Double {
         let stepDouble = Double(step)
         // Check if overridePercentage is not divisible by the selected step

+ 36 - 63
FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift

@@ -3,8 +3,9 @@ import CoreData
 import Foundation
 
 extension Adjustments.StateModel {
-    // MARK: - Setup the State variables with the last Temp Target configuration
+    // 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
@@ -13,29 +14,26 @@ extension Adjustments.StateModel {
             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 !isEnabled {
                 await resetTempTargetState()
             }
         } catch {
-            debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update latest temp target configuration"
-            )
+            debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to update latest temp target configuration")
         }
     }
 
-    // Sets the current active Preset name to show in the UI
+    /// Sets the current Temp Target for UI and logic purposes.
     @MainActor func setCurrentTempTarget(from IDs: [NSManagedObjectID]) async {
         do {
             guard let firstID = IDs.first else {
@@ -56,6 +54,9 @@ extension Adjustments.StateModel {
         }
     }
 
+    // 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
@@ -67,19 +68,19 @@ extension Adjustments.StateModel {
         }
     }
 
+    /// 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 extract Temp Targets as NSManagedObjects from the NSManagedObjectIDs with error: \(error.localizedDescription)"
-            )
+            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,
@@ -89,6 +90,7 @@ extension Adjustments.StateModel {
         )
     }
 
+    /// Sets up the scheduled Temp Targets array for the view.
     func setupScheduledTempTargetsArray() {
         setupTempTargets(
             fetchFunction: tempTargetStorage.fetchScheduledTempTargets,
@@ -98,10 +100,14 @@ extension Adjustments.StateModel {
         )
     }
 
+    // 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()
@@ -110,11 +116,9 @@ extension Adjustments.StateModel {
         }
     }
 
-    // Save scheduled Preset to Core Data
+    /// Saves a scheduled Temp Target and activates it at the specified date.
     func saveScheduledTempTarget() async {
-        // Save date to a constant to allow multiple executions of this function at the same time, i.e. allowing for scheduling multiple TTs
         let date = self.date
-
         guard date > Date() else { return }
 
         let tempTarget = TempTarget(
@@ -129,33 +133,24 @@ extension Adjustments.StateModel {
             enabled: false,
             halfBasalTarget: halfBasalTarget
         )
-
         await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
-
-        // Update Scheduled Temp Targets Array
         setupScheduledTempTargetsArray()
 
-        // If the scheduled date equals Date() enable the Preset
         Task {
-            // First wait until the time has passed
             await waitUntilDate(date)
-            // Then disable previous Temp Targets
             await disableAllActiveTempTargets(createTempTargetRunEntry: true)
-            // Set 'enabled' property to true, i.e. enacting it in Core Data
             await enableScheduledTempTarget(for: date)
-            // Activate the scheduled TT also for oref
             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 {
@@ -163,32 +158,27 @@ extension Adjustments.StateModel {
                 if let tempTarget = try viewContext.existingObject(with: firstID) as? TempTargetStored {
                     tempTarget.enabled = true
                     try viewContext.save()
-
-                    // Update Buttons in Adjustments View
                     isTempTargetEnabled = true
                 }
             } catch {
-                debugPrint("Failed to enable the Temp Target for the specified date: \(error.localizedDescription)")
+                debugPrint("Failed to enable the Temp Target: \(error.localizedDescription)")
             }
         }
-
-        // Refresh the list of scheduled Temp Targets
         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) // check every 60s
+            let sleepDuration = min(timeInterval, 60.0)
             try? await Task.sleep(nanoseconds: UInt64(sleepDuration * 1_000_000_000))
         }
     }
 
-    // Creates and enacts a non Preset Temp Target
+    /// Saves a custom Temp Target and disables existing ones.
     func saveCustomTempTarget() async {
-        // First disable all active TempTargets
         await disableAllActiveTempTargets(createTempTargetRunEntry: true)
-
         let tempTarget = TempTarget(
             name: tempTargetName,
             createdAt: date,
@@ -201,22 +191,14 @@ extension Adjustments.StateModel {
             enabled: true,
             halfBasalTarget: halfBasalTarget
         )
-
-        // Save Temp Target to Core Data
         await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
-
-        // Start Temp Target for oref
         tempTargetStorage.saveTempTargetsToStorage([tempTarget])
-
-        // Reset State variables
         await resetTempTargetState()
-
-        // Update View
         isTempTargetEnabled = true
         updateLatestTempTargetConfiguration()
     }
 
-    // Creates a new Temp Target Preset
+    /// Creates a new Temp Target preset.
     func saveTempTargetPreset() async {
         let tempTarget = TempTarget(
             name: tempTargetName,
@@ -230,47 +212,33 @@ extension Adjustments.StateModel {
             enabled: false,
             halfBasalTarget: halfBasalTarget
         )
-
-        // Save to Core Data
         await tempTargetStorage.storeTempTarget(tempTarget: tempTarget)
-
-        // Reset State variables
         await resetTempTargetState()
-
-        // Update View
         setupTempTargetPresetsArray()
     }
 
-    // Start Temp Target Preset
-    /// here we only have to update the Boolean Flag 'enabled'
+    /// Enacts a Temp Target preset by enabling it.
     @MainActor func enactTempTargetPreset(withID id: NSManagedObjectID) async {
         do {
-            /// get the underlying NSManagedObject of the Override that should be enabled
             let tempTargetToEnact = try viewContext.existingObject(with: id) as? TempTargetStored
             tempTargetToEnact?.enabled = true
             tempTargetToEnact?.date = Date()
             tempTargetToEnact?.isUploadedToNS = false
-
-            /// Update the 'Cancel Temp Target' button state
             isTempTargetEnabled = true
 
-            /// disable all active Temp Targets and reset state variables
             async let disableTempTargets: () = disableAllActiveTempTargets(
                 except: id,
                 createTempTargetRunEntry: currentActiveTempTarget != nil
             )
             async let resetState: () = resetTempTargetState()
-
             _ = await (disableTempTargets, resetState)
 
             if viewContext.hasChanges {
                 try viewContext.save()
             }
 
-            // Update View
             updateLatestTempTargetConfiguration()
 
-            // Map to TempTarget Struct
             let tempTarget = TempTarget(
                 name: tempTargetToEnact?.name,
                 createdAt: Date(),
@@ -283,15 +251,13 @@ extension Adjustments.StateModel {
                 enabled: true,
                 halfBasalTarget: halfBasalTarget
             )
-
-            // Make sure the Temp Target gets used by Oref
             tempTargetStorage.saveTempTargetsToStorage([tempTarget])
         } catch {
             debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to enact Override Preset")
         }
     }
 
-    // Disable all active Temp Targets
+    /// 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
@@ -344,6 +310,7 @@ extension Adjustments.StateModel {
         }
     }
 
+    /// 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,
@@ -353,7 +320,7 @@ extension Adjustments.StateModel {
         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
+        // 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
@@ -374,14 +341,13 @@ extension Adjustments.StateModel {
         }
     }
 
-    // Deletion of Temp Targets
+    /// Deletes a Temp Target preset.
     func invokeTempTargetPresetDeletion(_ objectID: NSManagedObjectID) async {
         await tempTargetStorage.deleteOverridePreset(objectID)
-
-        // Update Presets View
         setupTempTargetPresetsArray()
     }
 
+    /// Resets Temp Target state variables.
     @MainActor func resetTempTargetState() async {
         tempTargetName = ""
         tempTargetTarget = 100
@@ -390,6 +356,9 @@ extension Adjustments.StateModel {
         halfBasalTarget = settingHalfBasalTarget
     }
 
+    // MARK: - Calculations
+
+    /// Computes the half-basal target based on the current settings.
     func computeHalfBasalTarget(
         usingTarget initialTarget: Decimal? = nil,
         usingPercentage initialPercentage: Double? = nil
@@ -405,6 +374,7 @@ extension Adjustments.StateModel {
         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 }
@@ -412,6 +382,7 @@ extension Adjustments.StateModel {
         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
@@ -419,6 +390,7 @@ extension Adjustments.StateModel {
         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
@@ -426,6 +398,7 @@ extension Adjustments.StateModel {
         return maxSens
     }
 
+    /// Computes the adjusted percentage for the slider.
     func computeAdjustedPercentage(
         usingHBT initialHalfBasalTarget: Decimal? = nil,
         usingTarget initialTarget: Decimal? = nil

+ 54 - 146
FreeAPS/Sources/Modules/Adjustments/AdjustmentsStateModel.swift

@@ -5,12 +5,16 @@ import SwiftUI
 
 extension Adjustments {
     @Observable final class StateModel: BaseStateModel<Provider> {
+        // MARK: - Injected Dependencies
+
         @ObservationIgnored @Injected() var broadcaster: Broadcaster!
         @ObservationIgnored @Injected() var tempTargetStorage: TempTargetsStorage!
         @ObservationIgnored @Injected() var apsManager: APSManager!
         @ObservationIgnored @Injected() var overrideStorage: OverrideStorage!
         @ObservationIgnored @Injected() var nightscoutManager: NightscoutManager!
 
+        // MARK: - Override and Temp Target Properties
+
         var overridePercentage: Double = 100
         var isEnabled = false
         var indefinite = true
@@ -44,7 +48,7 @@ extension Adjustments {
         var showTempTargetEditSheet = false
         var units: GlucoseUnits = .mgdL
 
-        // temp target stuff
+        // Temp Target Properties
         let normalTarget: Decimal = 100
         var tempTargetDuration: Decimal = 0
         var tempTargetName: String = ""
@@ -63,14 +67,20 @@ extension Adjustments {
         var lowTTlowersSens: Bool = false
         var didSaveSettings: Bool = false
 
+        // Core Data
         let coredataContext = CoreDataStack.shared.newTaskContext()
         let viewContext = CoreDataStack.shared.persistentContainer.viewContext
 
+        // Help Sheet
         var isHelpSheetPresented: Bool = false
         var helpSheetDetent = PresentationDetent.large
 
+        // Combine
         private var cancellables = Set<AnyCancellable>()
 
+        // MARK: - Lifecycle
+
+        /// Subscribes to notifications and initializes settings.
         override func subscribe() {
             setupNotification()
             setupSettings()
@@ -79,22 +89,15 @@ extension Adjustments {
 
             Task {
                 await withTaskGroup(of: Void.self) { group in
-                    group.addTask {
-                        self.setupOverridePresetsArray()
-                    }
-                    group.addTask {
-                        self.setupTempTargetPresetsArray()
-                    }
-                    group.addTask {
-                        self.updateLatestOverrideConfiguration()
-                    }
-                    group.addTask {
-                        self.updateLatestTempTargetConfiguration()
-                    }
+                    group.addTask { self.setupOverridePresetsArray() }
+                    group.addTask { self.setupTempTargetPresetsArray() }
+                    group.addTask { self.updateLatestOverrideConfiguration() }
+                    group.addTask { self.updateLatestTempTargetConfiguration() }
                 }
             }
         }
 
+        /// Retrieves the current glucose target based on the time of day.
         func getCurrentGlucoseTarget() async {
             let now = Date()
             let calendar = Calendar.current
@@ -144,6 +147,7 @@ extension Adjustments {
             }
         }
 
+        /// Configures various settings from the settings manager.
         private func setupSettings() {
             units = settingsManager.settings.units
             defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
@@ -159,13 +163,44 @@ extension Adjustments {
                 await getCurrentGlucoseTarget()
             }
         }
+
+        /// Reorders Override Presets and updates the view.
+        func reorderOverride(from source: IndexSet, to destination: Int) {
+            overridePresets.move(fromOffsets: source, toOffset: destination)
+            for (index, override) in overridePresets.enumerated() {
+                override.orderPosition = Int16(index + 1)
+            }
+            do {
+                guard viewContext.hasChanges else { return }
+                try viewContext.save()
+                setupOverridePresetsArray()
+                Task { await nightscoutManager.uploadProfiles() }
+            } catch {
+                debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Override Presets order")
+            }
+        }
+
+        /// Reorders Temp Target Presets and updates the view.
+        func reorderTempTargets(from source: IndexSet, to destination: Int) {
+            tempTargetPresets.move(fromOffsets: source, toOffset: destination)
+            for (index, tempTarget) in tempTargetPresets.enumerated() {
+                tempTarget.orderPosition = Int16(index + 1)
+            }
+            do {
+                guard viewContext.hasChanges else { return }
+                try viewContext.save()
+                setupTempTargetPresetsArray()
+            } catch {
+                debugPrint("\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save Temp Target Presets order")
+            }
+        }
     }
 }
 
-// MARK: - Setup Notifications
+// MARK: - Notifications Setup
 
 extension Adjustments.StateModel {
-    // Custom Notification to update View when an Override has been cancelled via Home View
+    /// Sets up notification observers for Override and Temp Target updates.
     func setupNotification() {
         Foundation.NotificationCenter.default.addObserver(
             self,
@@ -199,60 +234,19 @@ extension Adjustments.StateModel {
             .store(in: &cancellables)
     }
 
+    /// Handles Override configuration updates.
     @objc private func handleOverrideConfigurationUpdate() {
         updateLatestOverrideConfiguration()
     }
 
+    /// Handles Temp Target configuration updates.
     @objc private func handleTempTargetConfigurationUpdate() {
         updateLatestTempTargetConfiguration()
     }
-
-    func reorderOverride(from source: IndexSet, to destination: Int) {
-        overridePresets.move(fromOffsets: source, toOffset: destination)
-
-        for (index, override) in overridePresets.enumerated() {
-            override.orderPosition = Int16(index + 1)
-        }
-
-        do {
-            guard viewContext.hasChanges else { return }
-            try viewContext.save()
-
-            // Update Presets View
-            setupOverridePresetsArray()
-
-            Task {
-                await nightscoutManager.uploadProfiles()
-            }
-        } catch {
-            debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save after reordering Override Presets with error: \(error.localizedDescription)"
-            )
-        }
-    }
-
-    func reorderTempTargets(from source: IndexSet, to destination: Int) {
-        tempTargetPresets.move(fromOffsets: source, toOffset: destination)
-
-        for (index, tempTarget) in tempTargetPresets.enumerated() {
-            tempTarget.orderPosition = Int16(index + 1)
-        }
-
-        do {
-            guard viewContext.hasChanges else { return }
-            try viewContext.save()
-
-            // Update Presets View
-            setupTempTargetPresetsArray()
-        } catch {
-            debugPrint(
-                "\(DebuggingIdentifiers.failed) \(#file) \(#function) Failed to save after reordering Temp Target Presets with error: \(error.localizedDescription)"
-            )
-        }
-    }
 }
 
 extension Adjustments.StateModel: SettingsObserver, PreferencesObserver {
+    /// Updates settings when they change.
     func settingsDidChange(_: FreeAPSSettings) {
         units = settingsManager.settings.units
         Task {
@@ -260,6 +254,7 @@ extension Adjustments.StateModel: SettingsObserver, PreferencesObserver {
         }
     }
 
+    /// Updates preferences when they change.
     func preferencesDidChange(_: Preferences) {
         defaultSmbMinutes = settingsManager.preferences.maxSMBBasalMinutes
         defaultUamMinutes = settingsManager.preferences.maxUAMSMBBasalMinutes
@@ -275,90 +270,3 @@ extension Adjustments.StateModel: SettingsObserver, PreferencesObserver {
         }
     }
 }
-
-extension PickerSettingsProvider {
-    func generatePickerValues(from setting: PickerSetting, units: GlucoseUnits, roundMinToStep: Bool) -> [Decimal] {
-        if !roundMinToStep {
-            return generatePickerValues(from: setting, units: units)
-        }
-
-        // Adjust min to be divisible by step
-        var newSetting = setting
-        var min = Double(newSetting.min)
-        let step = Double(newSetting.step)
-        let remainder = min.truncatingRemainder(dividingBy: step)
-        if remainder != 0 {
-            // Move min up to the next value divisible by targetStep
-            min += (step - remainder)
-        }
-
-        newSetting.min = Decimal(min)
-
-        return generatePickerValues(from: newSetting, units: units)
-    }
-}
-
-func percentageDescription(_ percent: Double) -> Text? {
-    if percent.isNaN || percent == 100 { return nil }
-
-    var description: String = "Insulin doses will be "
-
-    if percent < 100 {
-        description += "decreased by "
-    } else {
-        description += "increased by "
-    }
-
-    let deviationFrom100 = abs(percent - 100)
-    description += String(format: "%.0f% %.", deviationFrom100)
-
-    return Text(description)
-}
-
-// Function to check if the phone is using 24-hour format
-func is24HourFormat() -> Bool {
-    let formatter = DateFormatter()
-    formatter.locale = Locale.current
-    formatter.dateStyle = .none
-    formatter.timeStyle = .short
-    let dateString = formatter.string(from: Date())
-
-    return !dateString.contains("AM") && !dateString.contains("PM")
-}
-
-// Helper function to convert hours to AM/PM format
-func convertTo12HourFormat(_ hour: Int) -> String {
-    let formatter = DateFormatter()
-    formatter.dateFormat = "h a"
-
-    // Create a date from the hour and format it to AM/PM
-    let calendar = Calendar.current
-    let components = DateComponents(hour: hour)
-    let date = calendar.date(from: components) ?? Date()
-
-    return formatter.string(from: date)
-}
-
-// Helper function to format 24-hour numbers as two digits
-func format24Hour(_ hour: Int) -> String {
-    String(format: "%02d", hour)
-}
-
-func formatHrMin(_ durationInMinutes: Int) -> String {
-    let hours = durationInMinutes / 60
-    let minutes = durationInMinutes % 60
-
-    switch (hours, minutes) {
-    case let (0, m):
-        return "\(m) min"
-    case let (h, 0):
-        return "\(h) hr"
-    default:
-        return "\(hours) hr \(minutes) min"
-    }
-}
-
-func convertToMinutes(_ hours: Int, _ minutes: Int) -> Decimal {
-    let totalMinutes = (hours * 60) + minutes
-    return Decimal(max(0, totalMinutes))
-}

+ 1 - 1
FreeAPS/Sources/Modules/Adjustments/View/AdjustmentsRootView.swift

@@ -634,7 +634,7 @@ extension Adjustments {
 
             let targetString = target.isEmpty ? "" : "\(target) \(state.units.rawValue)"
 
-            let durationString = indefinite ? "" : "\(formatHrMin(Int(duration)))"
+            let durationString = indefinite ? "" : "\(state.formatHrMin(Int(duration)))"
 
             let scheduledSMBString: String = {
                 guard preset.smbIsScheduledOff, preset.start != preset.end else { return "" }

+ 18 - 12
FreeAPS/Sources/Modules/Adjustments/View/Overrides/AddOverrideForm.swift

@@ -102,7 +102,7 @@ struct AddOverrideForm: View {
             }
             .listRowBackground(Color.chart)
 
-            Section(footer: percentageDescription(state.overridePercentage)) {
+            Section(footer: state.percentageDescription(state.overridePercentage)) {
                 // Percentage Picker
                 HStack {
                     Text("Change Basal Rate by")
@@ -243,8 +243,8 @@ struct AddOverrideForm: View {
                         Text("From")
                         Spacer()
                         Text(
-                            is24HourFormat() ? format24Hour(Int(truncating: state.start as NSNumber)) + ":00" :
-                                convertTo12HourFormat(Int(truncating: state.start as NSNumber))
+                            state.is24HourFormat() ? state.format24Hour(Int(truncating: state.start as NSNumber)) + ":00" :
+                                state.convertTo12HourFormat(Int(truncating: state.start as NSNumber))
                         )
                         .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor)
                         Spacer()
@@ -253,8 +253,8 @@ struct AddOverrideForm: View {
                         Text("To")
                         Spacer()
                         Text(
-                            is24HourFormat() ? format24Hour(Int(truncating: state.end as NSNumber)) + ":00" :
-                                convertTo12HourFormat(Int(truncating: state.end as NSNumber))
+                            state.is24HourFormat() ? state.format24Hour(Int(truncating: state.end as NSNumber)) + ":00" :
+                                state.convertTo12HourFormat(Int(truncating: state.end as NSNumber))
                         )
                         .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor)
                         Spacer()
@@ -271,8 +271,11 @@ struct AddOverrideForm: View {
                                 set: { state.start = Decimal($0) }
                             ), label: Text("")) {
                                 ForEach(0 ..< 24, id: \.self) { hour in
-                                    Text(is24HourFormat() ? format24Hour(hour) + ":00" : convertTo12HourFormat(hour))
-                                        .tag(hour)
+                                    Text(
+                                        state.is24HourFormat() ? state.format24Hour(hour) + ":00" : state
+                                            .convertTo12HourFormat(hour)
+                                    )
+                                    .tag(hour)
                                 }
                             }
                             .pickerStyle(WheelPickerStyle())
@@ -284,8 +287,11 @@ struct AddOverrideForm: View {
                                 set: { state.end = Decimal($0) }
                             ), label: Text("")) {
                                 ForEach(0 ..< 24, id: \.self) { hour in
-                                    Text(is24HourFormat() ? format24Hour(hour) + ":00" : convertTo12HourFormat(hour))
-                                        .tag(hour)
+                                    Text(
+                                        state.is24HourFormat() ? state.format24Hour(hour) + ":00" : state
+                                            .convertTo12HourFormat(hour)
+                                    )
+                                    .tag(hour)
                                 }
                             }
                             .pickerStyle(WheelPickerStyle())
@@ -362,7 +368,7 @@ struct AddOverrideForm: View {
                     HStack {
                         Text("Duration")
                         Spacer()
-                        Text(formatHrMin(Int(state.overrideDuration)))
+                        Text(state.formatHrMin(Int(state.overrideDuration)))
                             .foregroundColor(!displayPickerDuration ? .primary : .accentColor)
                     }
                     .onTapGesture {
@@ -379,7 +385,7 @@ struct AddOverrideForm: View {
                             .pickerStyle(WheelPickerStyle())
                             .frame(maxWidth: .infinity)
                             .onChange(of: durationHours) {
-                                state.overrideDuration = convertToMinutes(durationHours, durationMinutes)
+                                state.overrideDuration = state.convertToMinutes(durationHours, durationMinutes)
                             }
 
                             Picker("Minutes", selection: $durationMinutes) {
@@ -390,7 +396,7 @@ struct AddOverrideForm: View {
                             .pickerStyle(WheelPickerStyle())
                             .frame(maxWidth: .infinity)
                             .onChange(of: durationMinutes) {
-                                state.overrideDuration = convertToMinutes(durationHours, durationMinutes)
+                                state.overrideDuration = state.convertToMinutes(durationHours, durationMinutes)
                             }
                         }
                         .listRowSeparator(.hidden, edges: .top)

+ 12 - 12
FreeAPS/Sources/Modules/Adjustments/View/Overrides/EditOverrideForm.swift

@@ -168,7 +168,7 @@ struct EditOverrideForm: View {
             }
 
             // Percentage Picker
-            Section(footer: percentageDescription(percentage)) {
+            Section(footer: state.percentageDescription(percentage)) {
                 HStack {
                     Text("Change Basal Rate by")
                     Spacer()
@@ -312,8 +312,8 @@ struct EditOverrideForm: View {
                         Text("From")
                         Spacer()
                         Text(
-                            is24HourFormat() ? format24Hour(Int(truncating: start! as NSNumber)) + ":00" :
-                                convertTo12HourFormat(Int(truncating: start! as NSNumber))
+                            state.is24HourFormat() ? state.format24Hour(Int(truncating: start! as NSNumber)) + ":00" :
+                                state.convertTo12HourFormat(Int(truncating: start! as NSNumber))
                         )
                         .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor)
 
@@ -326,8 +326,8 @@ struct EditOverrideForm: View {
                         Text("To")
                         Spacer()
                         Text(
-                            is24HourFormat() ? format24Hour(Int(truncating: end! as NSNumber)) + ":00" :
-                                convertTo12HourFormat(Int(truncating: end! as NSNumber))
+                            state.is24HourFormat() ? state.format24Hour(Int(truncating: end! as NSNumber)) + ":00" :
+                                state.convertTo12HourFormat(Int(truncating: end! as NSNumber))
                         )
                         .foregroundColor(!displayPickerDisableSmbSchedule ? .primary : .accentColor)
                     }
@@ -344,13 +344,13 @@ struct EditOverrideForm: View {
                                     hasChanges = true
                                 }
                             ), label: Text("")) {
-                                if is24HourFormat() {
+                                if state.is24HourFormat() {
                                     ForEach(0 ..< 24, id: \.self) { hour in
-                                        Text(format24Hour(hour) + ":00").tag(hour)
+                                        Text(state.format24Hour(hour) + ":00").tag(hour)
                                     }
                                 } else {
                                     ForEach(0 ..< 24, id: \.self) { hour in
-                                        Text(convertTo12HourFormat(hour)).tag(hour)
+                                        Text(state.convertTo12HourFormat(hour)).tag(hour)
                                     }
                                 }
                             }
@@ -364,13 +364,13 @@ struct EditOverrideForm: View {
                                     hasChanges = true
                                 }
                             ), label: Text("")) {
-                                if is24HourFormat() {
+                                if state.is24HourFormat() {
                                     ForEach(0 ..< 24, id: \.self) { hour in
-                                        Text(format24Hour(hour) + ":00").tag(hour)
+                                        Text(state.format24Hour(hour) + ":00").tag(hour)
                                     }
                                 } else {
                                     ForEach(0 ..< 24, id: \.self) { hour in
-                                        Text(convertTo12HourFormat(hour)).tag(hour)
+                                        Text(state.convertTo12HourFormat(hour)).tag(hour)
                                     }
                                 }
                             }
@@ -464,7 +464,7 @@ struct EditOverrideForm: View {
                     HStack {
                         Text("Duration")
                         Spacer()
-                        Text(formatHrMin(Int(truncating: duration as NSNumber)))
+                        Text(state.formatHrMin(Int(truncating: duration as NSNumber)))
                             .foregroundColor(!displayPickerDuration ? .primary : .accentColor)
                     }
                     .onTapGesture {

+ 2 - 2
FreeAPS/Sources/Modules/Adjustments/View/TempTargets/AddTempTargetForm.swift

@@ -162,7 +162,7 @@ struct AddTempTargetForm: View {
 
                 if state.isAdjustSensEnabled() {
                     Section(
-                        footer: percentageDescription(state.percentage),
+                        footer: state.percentageDescription(state.percentage),
                         content: {
                             Picker("Sensitivity Adjustment", selection: $tempTargetSensitivityAdjustmentType) {
                                 ForEach(TempTargetSensitivityAdjustmentType.allCases, id: \.self) { option in
@@ -213,7 +213,7 @@ struct AddTempTargetForm: View {
                     HStack {
                         Text("Duration")
                         Spacer()
-                        Text(formatHrMin(Int(state.tempTargetDuration)))
+                        Text(state.formatHrMin(Int(state.tempTargetDuration)))
                             .foregroundColor(
                                 !displayPickerDuration ?
                                     (state.tempTargetDuration > 0 ? .primary : .secondary) : .accentColor

+ 2 - 2
FreeAPS/Sources/Modules/Adjustments/View/TempTargets/EditTempTargetForm.swift

@@ -168,7 +168,7 @@ struct EditTempTargetForm: View {
 
                 if state.isAdjustSensEnabled(usingTarget: target) {
                     Section(
-                        footer: percentageDescription(percentage),
+                        footer: state.percentageDescription(percentage),
                         content: {
                             Picker("Sensitivity Adjustment", selection: $tempTargetSensitivityAdjustmentType) {
                                 ForEach(TempTargetSensitivityAdjustmentType.allCases, id: \.self) { option in
@@ -236,7 +236,7 @@ struct EditTempTargetForm: View {
                     HStack {
                         Text("Duration")
                         Spacer()
-                        Text(formatHrMin(Int(duration)))
+                        Text(state.formatHrMin(Int(duration)))
                             .foregroundColor(!displayPickerDuration ? (duration > 0 ? .primary : .secondary) : .accentColor)
                     }
                     .onTapGesture {

+ 26 - 0
FreeAPS/Sources/Modules/Home/View/HomeRootView.swift

@@ -1223,6 +1223,32 @@ extension UIScreen {
     }
 }
 
+/// Checks if the device is using a 24-hour time format.
+func is24HourFormat() -> Bool {
+    let formatter = DateFormatter()
+    formatter.locale = Locale.current
+    formatter.dateStyle = .none
+    formatter.timeStyle = .short
+    let dateString = formatter.string(from: Date())
+
+    return !dateString.contains("AM") && !dateString.contains("PM")
+}
+
+/// Converts a duration in minutes to a formatted string (e.g., "1 hr 30 min").
+func formatHrMin(_ durationInMinutes: Int) -> String {
+    let hours = durationInMinutes / 60
+    let minutes = durationInMinutes % 60
+
+    switch (hours, minutes) {
+    case let (0, m):
+        return "\(m) min"
+    case let (h, 0):
+        return "\(h) hr"
+    default:
+        return "\(hours) hr \(minutes) min"
+    }
+}
+
 // Helper function to convert a start and end hour to either 24-hour or AM/PM format
 func formatTimeRange(start: String?, end: String?) -> String {
     guard let start = start, let end = end else {