Bläddra i källkod

Merge pull request #343 from nightscout/beta-fixes-4

Beta Fixes #4
Deniz Cengiz 1 år sedan
förälder
incheckning
e1eea0bc24

+ 11 - 0
Model/Helper/TempTargetStored+Helper.swift

@@ -14,6 +14,17 @@ extension NSPredicate {
             true as NSNumber
         )
     }
+
+    static var tempTargetsForMainChart: NSPredicate {
+        let date = Date.oneDayAgo
+        return NSPredicate(
+            format: "(date >= %@ AND enabled == %@) OR (date >= %@ AND enabled == %@)",
+            date as NSDate,
+            true as NSNumber,
+            Date() as NSDate,
+            false as NSNumber
+        )
+    }
 }
 
 extension TempTargetStored {

+ 1 - 1
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -98473,7 +98473,7 @@
         "de" : {
           "stringUnit" : {
             "state" : "translated",
-            "value" : "Holzkohle"
+            "value" : "Kohlenhydrate hinzufügen"
           }
         },
         "es" : {

+ 1 - 0
Trio/Sources/Modules/Adjustments/AdjustmentsStateModel+Extensions/AdjustmentsStateModel+TempTargets.swift

@@ -366,6 +366,7 @@ extension Adjustments.StateModel {
     func invokeTempTargetPresetDeletion(_ objectID: NSManagedObjectID) async {
         await tempTargetStorage.deleteTempTargetPreset(objectID)
         setupTempTargetPresetsArray()
+        setupScheduledTempTargetsArray()
     }
 
     /// Resets Temp Target state variables.

+ 4 - 1
Trio/Sources/Modules/Adjustments/View/TempTargets/AddTempTargetForm.swift

@@ -252,8 +252,11 @@ struct AddTempTargetForm: View {
                             do {
                                 if noNameSpecified { state.tempTargetName = "Custom Target" }
                                 didPressSave.toggle()
-                                try await state.invokeSaveOfCustomTempTargets()
+
+                                /// We need to call dismiss() either before state.invokeSaveOfCustomTempTargets() or as a callback within the function BEFORE we await the Task, otherwise the sheet gets only closed when the scheduled Temp Target gets enacted
                                 dismiss()
+
+                                try await state.invokeSaveOfCustomTempTargets()
                             } catch {
                                 debug(.default, "\(DebuggingIdentifiers.failed) failed to save custom temp target: \(error)")
                             }

+ 23 - 7
Trio/Sources/Modules/DataTable/DataTableStateModel.swift

@@ -106,14 +106,21 @@ extension DataTable {
         func invokeCarbDeletionTask(_ treatmentObjectID: NSManagedObjectID, isFpuOrComplexMeal: Bool = false) {
             Task {
                 do {
-                    try await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: isFpuOrComplexMeal)
-
+                    /// Set the variables that control the CustomProgressView BEFORE the actual deletion
+                    /// otherwise the determineBasalSync gets executed first, sets waitForSuggestion to false and afterwards waitForSuggestion is set in this function to true, leading to an endless animation
                     await MainActor.run {
                         carbEntryDeleted = true
                         waitForSuggestion = true
                     }
+
+                    try await deleteCarbs(treatmentObjectID, isFpuOrComplexMeal: isFpuOrComplexMeal)
+
                 } catch {
                     debug(.default, "\(DebuggingIdentifiers.failed) Failed to delete carbs: \(error.localizedDescription)")
+                    await MainActor.run {
+                        carbEntryDeleted = false
+                        waitForSuggestion = false
+                    }
                 }
             }
         }
@@ -214,11 +221,6 @@ extension DataTable {
             Task {
                 do {
                     try await invokeInsulinDeletion(treatmentObjectID)
-
-                    await MainActor.run {
-                        insulinEntryDeleted = true
-                        waitForSuggestion = true
-                    }
                 } catch {
                     debug(.default, "\(DebuggingIdentifiers.failed) Failed to delete insulin entry: \(error)")
                 }
@@ -234,6 +236,16 @@ extension DataTable {
                     return
                 }
 
+                /// Set variables that control the CustomProgressView to true AFTER the authentication and BEFORE the actual determineBasalSync
+                /// We definitely need to set the variables BEFORE the actual sync
+                /// otherwise the determineBasalSync gets executed first, sets waitForSuggestion to false and afterwards waitForSuggestion is set in this function to true, leading to an endless animation
+                /// But we also want it AFTER the authentication
+                /// otherwise the animation would pop up even before the authentication prompt appears to the user
+                await MainActor.run {
+                    insulinEntryDeleted = true
+                    waitForSuggestion = true
+                }
+
                 // Delete from remote service(s) (i.e. Nightscout, Apple Health, Tidepool)
                 await deleteInsulinFromServices(with: treatmentObjectID)
 
@@ -246,6 +258,10 @@ extension DataTable {
                 debugPrint(
                     "\(DebuggingIdentifiers.failed) \(#file) \(#function) Error while Insulin Deletion Task: \(error.localizedDescription)"
                 )
+                await MainActor.run {
+                    insulinEntryDeleted = false
+                    waitForSuggestion = false
+                }
             }
         }
 

+ 1 - 1
Trio/Sources/Modules/Home/HomeStateModel+Setup/TempTargetSetup.swift

@@ -22,7 +22,7 @@ extension Home.StateModel {
         let results = try await CoreDataStack.shared.fetchEntitiesAsync(
             ofType: TempTargetStored.self,
             onContext: tempTargetFetchContext,
-            predicate: NSPredicate.lastActiveTempTarget,
+            predicate: NSPredicate.tempTargetsForMainChart,
             key: "date",
             ascending: false
         )

+ 60 - 35
Trio/Sources/Modules/Treatments/View/TreatmentsRootView.swift

@@ -80,15 +80,16 @@ extension Treatments {
             HStack {
                 HStack {
                     Text("Protein")
-
                     TextFieldWithToolBar(
                         text: $state.protein,
                         placeholder: "0",
                         keyboardType: .numberPad,
                         numberFormatter: mealFormatter,
-                        previousTextField: { focusOnPreviousTextField(index: 2) },
-                        nextTextField: { focusOnNextTextField(index: 2) }
-                    ).focused($focusedField, equals: .protein)
+                        showArrows: true,
+                        previousTextField: { focusedField = previousField(from: .protein) },
+                        nextTextField: { focusedField = nextField(from: .protein) }
+                    )
+                    .focused($focusedField, equals: .protein)
                     Text("g").foregroundColor(.secondary)
                 }
 
@@ -101,9 +102,11 @@ extension Treatments {
                         placeholder: "0",
                         keyboardType: .numberPad,
                         numberFormatter: mealFormatter,
-                        previousTextField: { focusOnPreviousTextField(index: 3) },
-                        nextTextField: { focusOnNextTextField(index: 3) }
-                    ).focused($focusedField, equals: .fat)
+                        showArrows: true,
+                        previousTextField: { focusedField = previousField(from: .fat) },
+                        nextTextField: { focusedField = nextField(from: .fat) }
+                    )
+                    .focused($focusedField, equals: .fat)
                     Text("g").foregroundColor(.secondary)
                 }
             }
@@ -118,39 +121,60 @@ extension Treatments {
                     placeholder: "0",
                     keyboardType: .numberPad,
                     numberFormatter: mealFormatter,
-                    previousTextField: { focusOnPreviousTextField(index: 1) },
-                    nextTextField: { focusOnNextTextField(index: 1) }
-                ).focused($focusedField, equals: .carbs)
-                    .onChange(of: state.carbs) {
-                        handleDebouncedInput()
-                    }
+                    showArrows: true,
+                    previousTextField: { focusedField = previousField(from: .carbs) },
+                    nextTextField: { focusedField = nextField(from: .carbs) }
+                )
+                .focused($focusedField, equals: .carbs)
+                .onChange(of: state.carbs) {
+                    handleDebouncedInput()
+                }
                 Text("g").foregroundColor(.secondary)
             }
         }
 
-        func focusOnPreviousTextField(index: Int) {
-            switch index {
-            case 2:
-                focusedField = .carbs
-            case 3:
-                focusedField = .fat
-            case 4:
-                focusedField = .protein
-            default:
-                break
+        /// Determines the next field to focus on based on the current focused field.
+        ///
+        /// This function handles the tab order navigation between input fields,
+        /// taking into account whether fat/protein fields are visible based on user settings.
+        ///
+        /// - Parameter current: The currently focused field
+        /// - Returns: The next field that should receive focus, or nil if there is no next field
+        private func nextField(from current: FocusedField) -> FocusedField? {
+            // If fat/protein fields are hidden, skip them in navigation
+            let showFPU = state.useFPUconversion
+
+            switch current {
+            case .fat:
+                return .bolus
+            case .protein:
+                return .fat
+            case .carbs:
+                return showFPU ? .protein : .bolus
+            case .bolus:
+                return .carbs
             }
         }
 
-        func focusOnNextTextField(index: Int) {
-            switch index {
-            case 1:
-                focusedField = .fat
-            case 2:
-                focusedField = .protein
-            case 3:
-                focusedField = .bolus
-            default:
-                break
+        /// Determines the previous field to focus on based on the current focused field.
+        ///
+        /// This function handles the reverse tab order navigation between input fields,
+        /// taking into account whether fat/protein fields are visible based on user settings.
+        ///
+        /// - Parameter current: The currently focused field
+        /// - Returns: The previous field that should receive focus, or nil if there is no previous field
+        private func previousField(from current: FocusedField) -> FocusedField? {
+            let showFPU = state.useFPUconversion
+
+            switch current {
+            case .fat:
+                return .protein
+            case .protein:
+                return .carbs
+            case .carbs:
+                return .bolus
+            case .bolus:
+                return showFPU ? .fat : .carbs
             }
         }
 
@@ -287,8 +311,9 @@ extension Treatments {
                                     textColor: colorScheme == .dark ? .white : .blue,
                                     maxLength: 5,
                                     numberFormatter: formatter,
-                                    previousTextField: { focusOnPreviousTextField(index: 4) },
-                                    nextTextField: { focusOnNextTextField(index: 4) }
+                                    showArrows: true,
+                                    previousTextField: { focusedField = previousField(from: .bolus) },
+                                    nextTextField: { focusedField = nextField(from: .bolus) }
                                 ).focused($focusedField, equals: .bolus)
                                     .onChange(of: state.amount) {
                                         Task {

+ 115 - 65
Trio/Sources/Views/TextFieldWithToolBar.swift

@@ -15,6 +15,7 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
     var textFieldDidBeginEditing: (() -> Void)?
     var numberFormatter: NumberFormatter
     var allowDecimalSeparator: Bool
+    var showArrows: Bool
     var previousTextField: (() -> Void)?
     var nextTextField: (() -> Void)?
 
@@ -32,6 +33,7 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
         textFieldDidBeginEditing: (() -> Void)? = nil,
         numberFormatter: NumberFormatter,
         allowDecimalSeparator: Bool = true,
+        showArrows: Bool = false,
         previousTextField: (() -> Void)? = nil,
         nextTextField: (() -> Void)? = nil
     ) {
@@ -49,6 +51,7 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
         self.numberFormatter = numberFormatter
         self.numberFormatter.numberStyle = .decimal
         self.allowDecimalSeparator = allowDecimalSeparator
+        self.showArrows = showArrows
         self.previousTextField = previousTextField
         self.nextTextField = nextTextField
     }
@@ -56,7 +59,7 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
     public func makeUIView(context: Context) -> UITextField {
         let textField = UITextField()
         context.coordinator.textField = textField
-        textField.inputAccessoryView = isDismissible ? makeDoneToolbar(for: textField, context: context) : nil
+        textField.inputAccessoryView = isDismissible ? createToolbar(for: textField, context: context) : nil
         textField.addTarget(context.coordinator, action: #selector(Coordinator.editingDidBegin), for: .editingDidBegin)
         textField.delegate = context.coordinator
         if text == 0 { /// show no value initially, i.e. empty String
@@ -68,36 +71,63 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
         return textField
     }
 
-    private func makeDoneToolbar(for textField: UITextField, context: Context) -> UIToolbar {
-        let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50))
-        let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
-        let doneButton = UIBarButtonItem(
-            image: UIImage(systemName: "keyboard.chevron.compact.down"),
-            style: .done,
-            target: textField,
-            action: #selector(UITextField.resignFirstResponder)
-        )
-        let clearButton = UIBarButtonItem(
-            image: UIImage(systemName: "trash"),
-            style: .plain,
-            target: context.coordinator,
-            action: #selector(Coordinator.clearText)
-        )
-        let previousButton = UIBarButtonItem(
-            image: UIImage(systemName: "chevron.up"),
-            style: .plain,
-            target: context.coordinator,
-            action: #selector(Coordinator.previousTextField)
-        )
-        let nextButton = UIBarButtonItem(
-            image: UIImage(systemName: "chevron.down"),
-            style: .plain,
-            target: context.coordinator,
-            action: #selector(Coordinator.nextTextField)
+    /// Creates and configures a toolbar for the text field with navigation and action buttons.
+    /// - Parameters:
+    ///   - _: The text field for which the toolbar is being created (unused parameter).
+    ///   - context: The SwiftUI context that contains the coordinator for handling button actions.
+    /// - Returns: A configured UIToolbar with appropriate buttons based on the view's configuration.
+    private func createToolbar(for _: UITextField, context: Context) -> UIToolbar {
+        let toolbar = UIToolbar()
+        var items: [UIBarButtonItem] = []
+
+        // Add navigation arrows if enabled
+        if showArrows {
+            // Add clear button
+            items.append(
+                UIBarButtonItem(
+                    image: UIImage(systemName: "trash"),
+                    style: .plain,
+                    target: context.coordinator,
+                    action: #selector(Coordinator.clearText)
+                )
+            )
+
+            if previousTextField != nil {
+                let previousButton = UIBarButtonItem(
+                    image: UIImage(systemName: "chevron.up"),
+                    style: .plain,
+                    target: context.coordinator,
+                    action: #selector(Coordinator.previousTextField)
+                )
+                items.append(previousButton)
+            }
+
+            if nextTextField != nil {
+                let nextButton = UIBarButtonItem(
+                    image: UIImage(systemName: "chevron.down"),
+                    style: .plain,
+                    target: context.coordinator,
+                    action: #selector(Coordinator.nextTextField)
+                )
+                items.append(nextButton)
+            }
+        }
+
+        // Add flexible space
+        items.append(UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil))
+
+        // Add done button
+        items.append(
+            UIBarButtonItem(
+                barButtonSystemItem: .done,
+                target: UIApplication.shared,
+                action: #selector(UIApplication.endEditing)
+            )
         )
 
-        toolbar.items = [clearButton, previousButton, nextButton, flexibleSpace, doneButton]
+        toolbar.items = items
         toolbar.sizeToFit()
+
         return toolbar
     }
 
@@ -185,6 +215,16 @@ public struct TextFieldWithToolBar: UIViewRepresentable {
 }
 
 extension TextFieldWithToolBar.Coordinator: UITextFieldDelegate {
+    public func textFieldDidEndEditing(_ textField: UITextField) {
+        if let text = textField.text,
+           let decimal = Decimal(string: text, locale: parent.numberFormatter.locale)
+        {
+            // Format the number properly when editing ends
+            textField.text = parent.numberFormatter.string(from: decimal as NSNumber)
+            parent.text = decimal
+        }
+    }
+
     public func textField(
         _ textField: UITextField,
         shouldChangeCharactersIn range: NSRange,
@@ -192,52 +232,57 @@ extension TextFieldWithToolBar.Coordinator: UITextFieldDelegate {
     ) -> Bool {
         // Check if the input is a number or the decimal separator
         let isNumber = CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: string))
-        let isDecimalSeparator = (string == decimalFormatter.decimalSeparator && textField.text?.contains(string) == false)
 
-        // Only proceed if the input is a valid number or decimal separator
-        if isNumber || isDecimalSeparator && parent.allowDecimalSeparator,
+        // Get the current locale's decimal separator
+        let currentDecimalSeparator = parent.numberFormatter.decimalSeparator ?? "."
+
+        // Check if input is a decimal separator (either . or ,)
+        let isInputDecimalSeparator = string == "." || string == ","
+
+        // Only allow the decimal separator configured in the locale
+        if isInputDecimalSeparator {
+            // If it's not the correct decimal separator for this locale, reject it
+            if string != currentDecimalSeparator {
+                return false
+            }
+            // Check if the field already contains a decimal separator
+            if textField.text?.contains(currentDecimalSeparator) == true {
+                return false
+            }
+        }
+
+        // Only proceed if the input is a valid number or the correct decimal separator
+        if isNumber || (string == currentDecimalSeparator && parent.allowDecimalSeparator),
            let currentText = textField.text as NSString?
         {
-            // Get the proposed new text
-            let proposedTextOriginal = currentText.replacingCharacters(in: range, with: string)
-
-            // Remove thousand separator
-            let proposedText = proposedTextOriginal.replacingOccurrences(of: decimalFormatter.groupingSeparator, with: "")
+            // Calculate the new text length
+            let newLength = currentText.length + string.count - range.length
 
-            // Try to convert proposed text to number
-            let number = parent.numberFormatter.number(from: proposedText) ?? decimalFormatter.number(from: proposedText)
+            // Check max length if specified
+            if let maxLength = parent.maxLength, newLength > maxLength {
+                return false
+            }
 
-            let decimalPlacesCurrent = calculateDecimalPlaces(in: currentText as String)
-            let maxDecimalPlaces = parent.numberFormatter.maximumFractionDigits
-            let isCursorAfterDecimal = isCursorAfterDecimal(in: textField, range: range)
+            // Create the new text string
+            let newText = currentText.replacingCharacters(in: range, with: string)
 
-            if decimalPlacesCurrent >= maxDecimalPlaces,
-               range.length == 0,
-               isCursorAfterDecimal
-            {
+            // If text starts with decimal separator, add leading zero
+            if newText.hasPrefix(currentDecimalSeparator) {
+                textField.text = "0" + newText
+                parent.text = Decimal(string: textField.text ?? "0") ?? 0
                 return false
             }
 
-            // Update the binding value if conversion is successful
-            if let number = number {
-                let lastCharIndex = proposedText.index(before: proposedText.endIndex)
-                let hasDecimalSeparator = proposedText.contains(decimalFormatter.decimalSeparator)
-                let hasTrailingZeros = (hasDecimalSeparator && proposedText[lastCharIndex] == "0") || isDecimalSeparator
-                if !hasTrailingZeros
-                {
-                    DispatchQueue.main.async {
-                        self.parent.text = number.decimalValue
-                    }
-                }
-            } else {
-                DispatchQueue.main.async {
-                    self.parent.text = 0
-                }
+            // Update the binding
+            if let decimal = Decimal(string: newText, locale: parent.numberFormatter.locale) {
+                parent.text = decimal
             }
+
+            return true
         }
 
-        // Allow the change if it's a valid number or decimal separator
-        return isNumber || isDecimalSeparator && parent.allowDecimalSeparator
+        // Allow the change if it's a valid number or the correct decimal separator
+        return isNumber || (string == currentDecimalSeparator && parent.allowDecimalSeparator)
     }
 
     public func textFieldDidBeginEditing(_: UITextField) {
@@ -254,7 +299,7 @@ extension UITextField {
 }
 
 extension UIApplication {
-    func endEditing() {
+    @objc func endEditing() {
         sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
     }
 }
@@ -273,7 +318,7 @@ public struct TextFieldWithToolBarString: UIViewRepresentable {
     public func makeUIView(context: Context) -> UITextField {
         let textField = UITextField()
         context.coordinator.textField = textField
-        textField.inputAccessoryView = isDismissible ? makeDoneToolbar(for: textField, context: context) : nil
+        textField.inputAccessoryView = isDismissible ? createToolbar(for: textField, context: context) : nil
         textField.addTarget(context.coordinator, action: #selector(Coordinator.editingDidBegin), for: .editingDidBegin)
         textField.delegate = context.coordinator
         textField.text = text
@@ -286,7 +331,12 @@ public struct TextFieldWithToolBarString: UIViewRepresentable {
         return textField
     }
 
-    private func makeDoneToolbar(for textField: UITextField, context: Context) -> UIToolbar {
+    /// Creates and configures a toolbar for the text field with clear and dismiss buttons.
+    /// - Parameters:
+    ///   - textField: The text field for which the toolbar is being created.
+    ///   - context: The SwiftUI context that contains the coordinator for handling button actions.
+    /// - Returns: A configured UIToolbar with clear and dismiss buttons.
+    private func createToolbar(for textField: UITextField, context: Context) -> UIToolbar {
         let toolbar = UIToolbar(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.width, height: 50))
         let flexibleSpace = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
         let doneButton = UIBarButtonItem(