ソースを参照

Merge pull request #337 from polscm32/decimalTextfield

Rework DecimalTextField Struct and Toolbar
Sjoerd Bozon 1 年間 前
コミット
bd56601448

+ 4 - 4
FreeAPS.xcodeproj/project.pbxproj

@@ -122,7 +122,7 @@
 		3871F38725ED661C0013ECB5 /* Suggestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3871F38625ED661C0013ECB5 /* Suggestion.swift */; };
 		3871F39C25ED892B0013ECB5 /* TempTarget.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3871F39B25ED892B0013ECB5 /* TempTarget.swift */; };
 		3871F39F25ED895A0013ECB5 /* Decimal+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3871F39E25ED895A0013ECB5 /* Decimal+Extensions.swift */; };
-		3883581C25EE79BB00E024B2 /* DecimalTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3883581B25EE79BB00E024B2 /* DecimalTextField.swift */; };
+		3883581C25EE79BB00E024B2 /* TextFieldWithToolBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3883581B25EE79BB00E024B2 /* TextFieldWithToolBar.swift */; };
 		3883583425EEB38000E024B2 /* PumpSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3883583325EEB38000E024B2 /* PumpSettings.swift */; };
 		388358C825EEF6D200E024B2 /* BasalProfileEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 388358C725EEF6D200E024B2 /* BasalProfileEntry.swift */; };
 		38887CCE25F5725200944304 /* IOBEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38887CCD25F5725200944304 /* IOBEntry.swift */; };
@@ -641,7 +641,7 @@
 		3871F38625ED661C0013ECB5 /* Suggestion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Suggestion.swift; sourceTree = "<group>"; };
 		3871F39B25ED892B0013ECB5 /* TempTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTarget.swift; sourceTree = "<group>"; };
 		3871F39E25ED895A0013ECB5 /* Decimal+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decimal+Extensions.swift"; sourceTree = "<group>"; };
-		3883581B25EE79BB00E024B2 /* DecimalTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalTextField.swift; sourceTree = "<group>"; };
+		3883581B25EE79BB00E024B2 /* TextFieldWithToolBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldWithToolBar.swift; sourceTree = "<group>"; };
 		3883583325EEB38000E024B2 /* PumpSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpSettings.swift; sourceTree = "<group>"; };
 		388358C725EEF6D200E024B2 /* BasalProfileEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasalProfileEntry.swift; sourceTree = "<group>"; };
 		38887CCD25F5725200944304 /* IOBEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOBEntry.swift; sourceTree = "<group>"; };
@@ -1516,7 +1516,7 @@
 			isa = PBXGroup;
 			children = (
 				3811DE5925C9D4D500A708ED /* ViewModifiers.swift */,
-				3883581B25EE79BB00E024B2 /* DecimalTextField.swift */,
+				3883581B25EE79BB00E024B2 /* TextFieldWithToolBar.swift */,
 				383420D825FFEB3F002D46C1 /* Popup.swift */,
 				389ECDFD2601061500D86C4F /* View+Snapshot.swift */,
 				38EA05FF262091870064E39B /* BolusProgressViewStyle.swift */,
@@ -2719,7 +2719,7 @@
 				38A504A425DD9C4000C5B9E8 /* UserDefaultsExtensions.swift in Sources */,
 				38FE826A25CC82DB001FF17A /* NetworkService.swift in Sources */,
 				FE66D16B291F74F8005D6F77 /* Bundle+Extensions.swift in Sources */,
-				3883581C25EE79BB00E024B2 /* DecimalTextField.swift in Sources */,
+				3883581C25EE79BB00E024B2 /* TextFieldWithToolBar.swift in Sources */,
 				6B1A8D2E2B156EEF00E76752 /* LiveActivityBridge.swift in Sources */,
 				38DAB28A260D349500F74C1A /* FetchGlucoseManager.swift in Sources */,
 				38F37828261260DC009DB701 /* Color+Extensions.swift in Sources */,

+ 22 - 27
FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift

@@ -42,12 +42,11 @@ extension AddCarbs {
                     HStack {
                         Text("Carbs").fontWeight(.semibold)
                         Spacer()
-                        DecimalTextField(
-                            "0",
-                            value: $state.carbs,
-                            formatter: formatter,
-                            autofocus: true,
-                            cleanInput: true
+                        TextFieldWithToolBar(
+                            text: $state.carbs,
+                            placeholder: "0",
+                            shouldBecomeFirstResponder: true,
+                            numberFormatter: formatter
                         )
                         Text(state.carbs > state.maxCarbs ? "⚠️" : "g").foregroundColor(.secondary)
                     }.padding(.vertical)
@@ -55,14 +54,22 @@ extension AddCarbs {
                     if state.useFPUconversion {
                         proteinAndFat()
                     }
-                    HStack {
-                        Text("Note").foregroundColor(.secondary)
-                        TextField("", text: $state.note).multilineTextAlignment(.trailing)
-                        if isFocused {
-                            Button { isFocused = false } label: { Image(systemName: "keyboard.chevron.compact.down") }
-                                .controlSize(.mini)
+                    VStack {
+                        HStack {
+                            Text("Note").foregroundColor(.secondary)
+                            TextFieldWithToolBarString(text: $state.note, placeholder: "", maxLength: 25)
+                            if isFocused {
+                                Button { isFocused = false } label: { Image(systemName: "keyboard.chevron.compact.down") }
+                                    .controlSize(.mini)
+                            }
+                        }.focused($isFocused)
+
+                        HStack {
+                            Spacer()
+                            Text("\(state.note.count) / 25")
+                                .foregroundStyle(.secondary)
                         }
-                    }.focused($isFocused)
+                    }
                     HStack {
                         Button {
                             state.useFPUconversion.toggle()
@@ -268,25 +275,13 @@ extension AddCarbs {
             HStack {
                 Text("Fat").foregroundColor(.orange) // .fontWeight(.thin)
                 Spacer()
-                DecimalTextField(
-                    "0",
-                    value: $state.fat,
-                    formatter: formatter,
-                    autofocus: false,
-                    cleanInput: true
-                )
+                TextFieldWithToolBar(text: $state.fat, placeholder: "0", numberFormatter: formatter)
                 Text(state.fat > state.maxFat ? "⚠️" : "g").foregroundColor(.secondary)
             }
             HStack {
                 Text("Protein").foregroundColor(.red) // .fontWeight(.thin)
                 Spacer()
-                DecimalTextField(
-                    "0",
-                    value: $state.protein,
-                    formatter: formatter,
-                    autofocus: false,
-                    cleanInput: true
-                )
+                TextFieldWithToolBar(text: $state.protein, placeholder: "0", numberFormatter: formatter)
                 Text(state.protein > state.maxProtein ? "⚠️" : "g").foregroundColor(.secondary)
             }
         }

+ 3 - 3
FreeAPS/Sources/Modules/AddTempTarget/View/AddTempTargetRootView.swift

@@ -187,13 +187,13 @@ extension AddTempTarget {
                     HStack {
                         Text("Target")
                         Spacer()
-                        DecimalTextField("0", value: $state.low, formatter: formatter, cleanInput: true)
+                        TextFieldWithToolBar(text: $state.low, placeholder: "0", numberFormatter: formatter)
                         Text(state.units.rawValue).foregroundColor(.secondary)
                     }
                     HStack {
                         Text("Duration")
                         Spacer()
-                        DecimalTextField("0", value: $state.duration, formatter: formatter, cleanInput: true)
+                        TextFieldWithToolBar(text: $state.duration, placeholder: "0", numberFormatter: formatter)
                         Text("minutes").foregroundColor(.secondary)
                     }
                 }
@@ -203,7 +203,7 @@ extension AddTempTarget {
                     HStack {
                         Text("Duration")
                         Spacer()
-                        DecimalTextField("0", value: $state.duration, formatter: formatter, cleanInput: true)
+                        TextFieldWithToolBar(text: $state.duration, placeholder: "0", numberFormatter: formatter)
                         Text("minutes").foregroundColor(.secondary)
                     }
                 }

+ 5 - 6
FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift

@@ -65,12 +65,11 @@ extension Bolus {
                         HStack {
                             Text("Amount")
                             Spacer()
-                            DecimalTextField(
-                                "0",
-                                value: $state.amount,
-                                formatter: formatter,
-                                autofocus: true,
-                                cleanInput: true
+                            TextFieldWithToolBar(
+                                text: $state.amount,
+                                placeholder: "0",
+                                shouldBecomeFirstResponder: true,
+                                numberFormatter: formatter
                             )
                             Text(state.amount > state.maxBolus ? "⚠️" : "U").foregroundColor(.secondary)
                         }

+ 1 - 7
FreeAPS/Sources/Modules/Calibrations/View/CalibrationsRootView.swift

@@ -27,13 +27,7 @@ extension Calibrations {
                         HStack {
                             Text("Meter glucose")
                             Spacer()
-                            DecimalTextField(
-                                "0",
-                                value: $state.newCalibration,
-                                formatter: formatter,
-                                autofocus: false,
-                                cleanInput: true
-                            )
+                            TextFieldWithToolBar(text: $state.newCalibration, placeholder: "0", numberFormatter: formatter)
                             Text(state.units.rawValue).foregroundColor(.secondary)
                         }
                         Button {

+ 10 - 12
FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift

@@ -140,12 +140,11 @@ extension DataTable {
                         Section {
                             HStack {
                                 Text("New Glucose")
-                                DecimalTextField(
-                                    " ... ",
-                                    value: $state.manualGlucose,
-                                    formatter: glucoseFormatter,
-                                    autofocus: true,
-                                    cleanInput: true
+                                TextFieldWithToolBar(
+                                    text: $state.manualGlucose,
+                                    placeholder: " ... ",
+                                    shouldBecomeFirstResponder: true,
+                                    numberFormatter: glucoseFormatter
                                 )
                                 Text(state.units.rawValue).foregroundStyle(.secondary)
                             }
@@ -253,12 +252,11 @@ extension DataTable {
                             HStack {
                                 Text("Amount")
                                 Spacer()
-                                DecimalTextField(
-                                    "0",
-                                    value: $state.externalInsulinAmount,
-                                    formatter: insulinFormatter,
-                                    autofocus: true,
-                                    cleanInput: true
+                                TextFieldWithToolBar(
+                                    text: $state.externalInsulinAmount,
+                                    placeholder: "0",
+                                    shouldBecomeFirstResponder: true,
+                                    numberFormatter: insulinFormatter
                                 )
                                 Text("U").foregroundColor(.secondary)
                             }

+ 11 - 7
FreeAPS/Sources/Modules/FPUConfig/View/FPUConfigRootView.swift

@@ -31,15 +31,15 @@ extension FPUConfig {
                 Section(header: Text("Limit Per Entry")) {
                     HStack {
                         Text("Max Carbs")
-                        DecimalTextField("g", value: $state.maxCarbs, formatter: formatter)
+                        TextFieldWithToolBar(text: $state.maxCarbs, placeholder: "g", numberFormatter: formatter)
                     }
                     HStack {
                         Text("Max Fat")
-                        DecimalTextField("g", value: $state.maxFat, formatter: formatter)
+                        TextFieldWithToolBar(text: $state.maxFat, placeholder: "g", numberFormatter: formatter)
                     }
                     HStack {
                         Text("Max Protein")
-                        DecimalTextField("g", value: $state.maxProtein, formatter: formatter)
+                        TextFieldWithToolBar(text: $state.maxProtein, placeholder: "g", numberFormatter: formatter)
                     }
                 }
 
@@ -47,22 +47,26 @@ extension FPUConfig {
                     HStack {
                         Text("Delay In Minutes")
                         Spacer()
-                        DecimalTextField("60", value: $state.delay, formatter: intFormater)
+                        TextFieldWithToolBar(text: $state.delay, placeholder: "60", numberFormatter: intFormater)
                     }
                     HStack {
                         Text("Maximum Duration In Hours")
                         Spacer()
-                        DecimalTextField("8", value: $state.timeCap, formatter: intFormater)
+                        TextFieldWithToolBar(text: $state.timeCap, placeholder: "8", numberFormatter: intFormater)
                     }
                     HStack {
                         Text("Interval In Minutes")
                         Spacer()
-                        DecimalTextField("30", value: $state.minuteInterval, formatter: intFormater)
+                        TextFieldWithToolBar(text: $state.minuteInterval, placeholder: "30", numberFormatter: intFormater)
                     }
                     HStack {
                         Text("Override With A Factor Of ")
                         Spacer()
-                        DecimalTextField("0.5", value: $state.individualAdjustmentFactor, formatter: conversionFormatter)
+                        TextFieldWithToolBar(
+                            text: $state.individualAdjustmentFactor,
+                            placeholder: "0.5",
+                            numberFormatter: conversionFormatter
+                        )
                     }
                 }
 

+ 6 - 1
FreeAPS/Sources/Modules/ManualTempBasal/View/ManualTempBasalRootView.swift

@@ -19,7 +19,12 @@ extension ManualTempBasal {
                     HStack {
                         Text("Amount")
                         Spacer()
-                        DecimalTextField("0", value: $state.rate, formatter: formatter, autofocus: true, cleanInput: true)
+                        TextFieldWithToolBar(
+                            text: $state.rate,
+                            placeholder: "0",
+                            shouldBecomeFirstResponder: true,
+                            numberFormatter: formatter
+                        )
                         Text("U/hr").foregroundColor(.secondary)
                     }
                     Picker(selection: $state.durationIndex, label: Text("Duration")) {

+ 11 - 4
FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConnectView.swift

@@ -2,12 +2,13 @@ import SwiftUI
 
 struct NightscoutConnectView: View {
     @ObservedObject var state: NightscoutConfig.StateModel
-    @State private var portFormater: NumberFormatter
+    @State private var portFormatter: NumberFormatter
 
     init(state: NightscoutConfig.StateModel) {
         self.state = state
-        portFormater = NumberFormatter()
-        portFormater.allowsFloats = false
+        portFormatter = NumberFormatter()
+        portFormatter.allowsFloats = false
+        portFormatter.usesGroupingSeparator = false
     }
 
     var body: some View {
@@ -49,7 +50,13 @@ struct NightscoutConnectView: View {
                 Toggle("Use local glucose server", isOn: $state.useLocalSource)
                 HStack {
                     Text("Port")
-                    DecimalTextField("", value: $state.localPort, formatter: portFormater)
+                    TextFieldWithToolBar(
+                        text: $state.localPort,
+                        placeholder: "",
+                        keyboardType: .numberPad,
+                        numberFormatter: portFormatter,
+                        allowDecimalSeparator: false
+                    )
                 }
             } header: { Text("Local glucose source") }
         }

+ 7 - 3
FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift

@@ -89,14 +89,14 @@ extension NotificationsConfig {
                     HStack {
                         Text("Low")
                         Spacer()
-                        DecimalTextField("0", value: $state.lowGlucose, formatter: glucoseFormatter)
+                        TextFieldWithToolBar(text: $state.lowGlucose, placeholder: "0", numberFormatter: glucoseFormatter)
                         Text(state.units.rawValue).foregroundColor(.secondary)
                     }
 
                     HStack {
                         Text("High")
                         Spacer()
-                        DecimalTextField("0", value: $state.highGlucose, formatter: glucoseFormatter)
+                        TextFieldWithToolBar(text: $state.highGlucose, placeholder: "0", numberFormatter: glucoseFormatter)
                         Text(state.units.rawValue).foregroundColor(.secondary)
                     }
                 }
@@ -105,7 +105,11 @@ extension NotificationsConfig {
                     HStack {
                         Text("Carbs Required Threshold")
                         Spacer()
-                        DecimalTextField("0", value: $state.carbsRequiredThreshold, formatter: carbsFormatter)
+                        TextFieldWithToolBar(
+                            text: $state.carbsRequiredThreshold,
+                            placeholder: "0",
+                            numberFormatter: carbsFormatter
+                        )
                         Text("g").foregroundColor(.secondary)
                     }
                 }

+ 6 - 16
FreeAPS/Sources/Modules/OverrideProfilesConfig/View/OverrideProfilesRootView.swift

@@ -135,7 +135,7 @@ extension OverrideProfilesConfig {
                 if !state._indefinite {
                     HStack {
                         Text("Duration")
-                        DecimalTextField("0", value: $state.duration, formatter: formatter, cleanInput: false)
+                        TextFieldWithToolBar(text: $state.duration, placeholder: "0", numberFormatter: formatter)
                         Text("minutes").foregroundColor(.secondary)
                     }
                 }
@@ -148,7 +148,7 @@ extension OverrideProfilesConfig {
                 if state.override_target {
                     HStack {
                         Text("Target Glucose")
-                        DecimalTextField("0", value: $state.target, formatter: glucoseFormatter, cleanInput: false)
+                        TextFieldWithToolBar(text: $state.target, placeholder: "0", numberFormatter: glucoseFormatter)
                         Text(state.units.rawValue).foregroundColor(.secondary)
                     }
                 }
@@ -172,12 +172,12 @@ extension OverrideProfilesConfig {
                         if state.smbIsScheduledOff {
                             HStack {
                                 Text("First Hour SMBs are Off (24 hours)")
-                                DecimalTextField("0", value: $state.start, formatter: formatter, cleanInput: false)
+                                TextFieldWithToolBar(text: $state.start, placeholder: "0", numberFormatter: formatter)
                                 Text("hour").foregroundColor(.secondary)
                             }
                             HStack {
                                 Text("First Hour SMBs are Resumed (24 hours)")
-                                DecimalTextField("0", value: $state.end, formatter: formatter, cleanInput: false)
+                                TextFieldWithToolBar(text: $state.end, placeholder: "0", numberFormatter: formatter)
                                 Text("hour").foregroundColor(.secondary)
                             }
                         }
@@ -201,22 +201,12 @@ extension OverrideProfilesConfig {
                     }
                     HStack {
                         Text("SMB Minutes")
-                        DecimalTextField(
-                            "0",
-                            value: $state.smbMinutes,
-                            formatter: formatter,
-                            cleanInput: false
-                        )
+                        TextFieldWithToolBar(text: $state.smbMinutes, placeholder: "0", numberFormatter: formatter)
                         Text("minutes").foregroundColor(.secondary)
                     }
                     HStack {
                         Text("UAM SMB Minutes")
-                        DecimalTextField(
-                            "0",
-                            value: $state.uamMinutes,
-                            formatter: formatter,
-                            cleanInput: false
-                        )
+                        TextFieldWithToolBar(text: $state.uamMinutes, placeholder: "0", numberFormatter: formatter)
                         Text("minutes").foregroundColor(.secondary)
                     }
                 }

+ 5 - 5
FreeAPS/Sources/Modules/PreferencesEditor/View/PreferencesEditorRootView.swift

@@ -29,7 +29,7 @@ extension PreferencesEditor {
                     }
                     HStack {
                         Text("Recommended Bolus Percentage")
-                        DecimalTextField("", value: $state.insulinReqPercentage, formatter: formatter)
+                        TextFieldWithToolBar(text: $state.insulinReqPercentage, placeholder: "", numberFormatter: formatter)
                     }
 
                     Toggle("Skip Bolus screen after carbs", isOn: $state.skipBolusScreenAfterCarbs)
@@ -62,10 +62,10 @@ extension PreferencesEditor {
                                         })
                                         Text(field.displayName)
                                     }
-                                    DecimalTextField(
-                                        "0",
-                                        value: self.$state.sections[sectionIndex].fields[fieldIndex].decimalValue,
-                                        formatter: formatter
+                                    TextFieldWithToolBar(
+                                        text: self.$state.sections[sectionIndex].fields[fieldIndex].decimalValue,
+                                        placeholder: "0",
+                                        numberFormatter: formatter
                                     )
                                 case .insulinCurve:
                                     Picker(

+ 3 - 3
FreeAPS/Sources/Modules/PumpSettingsEditor/View/PumpSettingsEditorRootView.swift

@@ -17,18 +17,18 @@ extension PumpSettingsEditor {
                 Section(header: Text("Delivery limits")) {
                     HStack {
                         Text("Max Basal")
-                        DecimalTextField("U/hr", value: $state.maxBasal, formatter: formatter)
+                        TextFieldWithToolBar(text: $state.maxBasal, placeholder: "U/hr", numberFormatter: formatter)
                     }
                     HStack {
                         Text("Max Bolus")
-                        DecimalTextField("U", value: $state.maxBolus, formatter: formatter)
+                        TextFieldWithToolBar(text: $state.maxBolus, placeholder: "U", numberFormatter: formatter)
                     }
                 }
 
                 Section(header: Text("Duration of Insulin Action")) {
                     HStack {
                         Text("DIA")
-                        DecimalTextField("hours", value: $state.dia, formatter: formatter)
+                        TextFieldWithToolBar(text: $state.dia, placeholder: "hours", numberFormatter: formatter)
                     }
                 }
 

+ 3 - 3
FreeAPS/Sources/Modules/StatConfig/View/StatConfigRootView.swift

@@ -36,21 +36,21 @@ extension StatConfig {
                     HStack {
                         Text("Hours X-Axis (6 default)")
                         Spacer()
-                        DecimalTextField("6", value: $state.hours, formatter: carbsFormatter)
+                        TextFieldWithToolBar(text: $state.hours, placeholder: "6", numberFormatter: carbsFormatter)
                         Text("hours").foregroundColor(.secondary)
                     }
 
                     HStack {
                         Text("Low")
                         Spacer()
-                        DecimalTextField("0", value: $state.low, formatter: glucoseFormatter)
+                        TextFieldWithToolBar(text: $state.low, placeholder: "0", numberFormatter: glucoseFormatter)
                         Text(state.units.rawValue).foregroundColor(.secondary)
                     }
 
                     HStack {
                         Text("High")
                         Spacer()
-                        DecimalTextField("0", value: $state.high, formatter: glucoseFormatter)
+                        TextFieldWithToolBar(text: $state.high, placeholder: "0", numberFormatter: glucoseFormatter)
                         Text(state.units.rawValue).foregroundColor(.secondary)
                     }
                 }

+ 0 - 160
FreeAPS/Sources/Views/DecimalTextField.swift

@@ -1,160 +0,0 @@
-import Combine
-import SwiftUI
-
-struct DecimalTextField: UIViewRepresentable {
-    private var placeholder: String
-    @Binding var value: Decimal
-    private var formatter: NumberFormatter
-    private var autofocus: Bool
-    private var cleanInput: Bool
-
-    init(
-        _ placeholder: String,
-        value: Binding<Decimal>,
-        formatter: NumberFormatter,
-        autofocus: Bool = false,
-        cleanInput: Bool = false
-    ) {
-        self.placeholder = placeholder
-        _value = value
-        self.formatter = formatter
-        self.autofocus = autofocus
-        self.cleanInput = cleanInput
-    }
-
-    func makeUIView(context: Context) -> UITextField {
-        let textfield = UITextField()
-        textfield.keyboardType = .decimalPad
-        textfield.delegate = context.coordinator
-        textfield.placeholder = placeholder
-        textfield.text = cleanInput ? "" : formatter.string(for: value) ?? placeholder
-        textfield.textAlignment = .right
-
-        let toolBar = UIToolbar(frame: CGRect(
-            x: 0,
-            y: 0,
-            width: textfield.frame.size.width,
-            height: 44
-        ))
-        let clearButton = UIBarButtonItem(
-            title: NSLocalizedString("Clear", comment: "Clear button"),
-            style: .plain,
-            target: self,
-            action: #selector(textfield.clearButtonTapped(button:))
-        )
-        let doneButton = UIBarButtonItem(
-            title: NSLocalizedString("Done", comment: "Done button"),
-            style: .done,
-            target: self,
-            action: #selector(textfield.doneButtonTapped(button:))
-        )
-        let space = UIBarButtonItem(
-            barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace,
-            target: nil,
-            action: nil
-        )
-        toolBar.setItems([clearButton, space, doneButton], animated: true)
-        textfield.inputAccessoryView = toolBar
-        if autofocus {
-            DispatchQueue.main.async {
-                textfield.becomeFirstResponder()
-            }
-        }
-        return textfield
-    }
-
-    func updateUIView(_ textField: UITextField, context: Context) {
-        let coordinator = context.coordinator
-        if coordinator.isEditing {
-            coordinator.resetEditing()
-        } else if value == 0 {
-            textField.text = ""
-        } else {
-            textField.text = formatter.string(for: value)
-        }
-    }
-
-    func makeCoordinator() -> Coordinator {
-        Coordinator(self)
-    }
-
-    class Coordinator: NSObject, UITextFieldDelegate {
-        var parent: DecimalTextField
-
-        init(_ textField: DecimalTextField) {
-            parent = textField
-        }
-
-        private(set) var isEditing = false
-        private var editingCancellable: AnyCancellable?
-
-        func resetEditing() {
-            editingCancellable = Just(false)
-                .delay(for: 0.5, scheduler: DispatchQueue.main)
-                .weakAssign(to: \.isEditing, on: self)
-        }
-
-        func textField(
-            _ textField: UITextField,
-            shouldChangeCharactersIn range: NSRange,
-            replacementString string: String
-        ) -> Bool {
-            // Allow only numbers and decimal characters
-            let isNumber = CharacterSet.decimalDigits.isSuperset(of: CharacterSet(charactersIn: string))
-            let withDecimal = (
-                string == NumberFormatter().decimalSeparator &&
-                    textField.text?.contains(string) == false
-            )
-
-            if isNumber || withDecimal,
-               let currentValue = textField.text as NSString?
-            {
-                // Update Value
-                let proposedValue = currentValue.replacingCharacters(in: range, with: string) as String
-
-                let decimalFormatter = NumberFormatter()
-                decimalFormatter.locale = Locale.current
-                decimalFormatter.numberStyle = .decimal
-
-                // Try currency formatter then Decimal formatrer
-                let number = parent.formatter.number(from: proposedValue) ?? decimalFormatter.number(from: proposedValue) ?? 0.0
-
-                // Set Value
-                let double = number.doubleValue
-                isEditing = true
-                parent.value = Decimal(double)
-            }
-
-            return isNumber || withDecimal
-        }
-
-        func textFieldDidEndEditing(
-            _ textField: UITextField,
-            reason _: UITextField.DidEndEditingReason
-        ) {
-            // Format value with formatter at End Editing
-            textField.text = parent.formatter.string(for: parent.value)
-            isEditing = false
-        }
-    }
-}
-
-// MARK: extension for done button
-
-extension UITextField {
-    @objc func doneButtonTapped(button _: UIBarButtonItem) {
-        resignFirstResponder()
-    }
-
-    @objc func clearButtonTapped(button _: UIBarButtonItem) {
-        text = ""
-    }
-}
-
-// MARK: extension for keyboard to dismiss
-
-extension UIApplication {
-    func endEditing() {
-        sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
-    }
-}

+ 311 - 0
FreeAPS/Sources/Views/TextFieldWithToolBar.swift

@@ -0,0 +1,311 @@
+import SwiftUI
+import UIKit
+
+public struct TextFieldWithToolBar: UIViewRepresentable {
+    @Binding var text: Decimal
+    var placeholder: String
+    var textColor: UIColor
+    var textAlignment: NSTextAlignment
+    var keyboardType: UIKeyboardType
+    var autocapitalizationType: UITextAutocapitalizationType
+    var autocorrectionType: UITextAutocorrectionType
+    var shouldBecomeFirstResponder: Bool
+    var maxLength: Int?
+    var isDismissible: Bool
+    var textFieldDidBeginEditing: (() -> Void)?
+    var numberFormatter: NumberFormatter
+    var allowDecimalSeparator: Bool
+
+    public init(
+        text: Binding<Decimal>,
+        placeholder: String,
+        textColor: UIColor = .label,
+        textAlignment: NSTextAlignment = .right,
+        keyboardType: UIKeyboardType = .decimalPad,
+        autocapitalizationType: UITextAutocapitalizationType = .none,
+        autocorrectionType: UITextAutocorrectionType = .no,
+        shouldBecomeFirstResponder: Bool = false,
+        maxLength: Int? = nil,
+        isDismissible: Bool = true,
+        textFieldDidBeginEditing: (() -> Void)? = nil,
+        numberFormatter: NumberFormatter,
+        allowDecimalSeparator: Bool = true
+    ) {
+        _text = text
+        self.placeholder = placeholder
+        self.textColor = textColor
+        self.textAlignment = textAlignment
+        self.keyboardType = keyboardType
+        self.autocapitalizationType = autocapitalizationType
+        self.autocorrectionType = autocorrectionType
+        self.shouldBecomeFirstResponder = shouldBecomeFirstResponder
+        self.maxLength = maxLength
+        self.isDismissible = isDismissible
+        self.textFieldDidBeginEditing = textFieldDidBeginEditing
+        self.numberFormatter = numberFormatter
+        self.numberFormatter.numberStyle = .decimal
+        self.allowDecimalSeparator = allowDecimalSeparator
+    }
+
+    public func makeUIView(context: Context) -> UITextField {
+        let textField = UITextField()
+        context.coordinator.textField = textField
+        textField.inputAccessoryView = isDismissible ? makeDoneToolbar(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
+            textField.text = ""
+        } else {
+            textField.text = numberFormatter.string(for: text)
+        }
+        textField.placeholder = placeholder
+        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)
+        )
+
+        toolbar.items = [clearButton, flexibleSpace, doneButton]
+        toolbar.sizeToFit()
+        return toolbar
+    }
+
+    public func updateUIView(_ textField: UITextField, context: Context) {
+        if text != 0 {
+            let newText = numberFormatter.string(for: text) ?? ""
+            if textField.text != newText {
+                textField.text = newText
+            }
+        }
+
+        textField.textColor = textColor
+        textField.textAlignment = textAlignment
+        textField.keyboardType = keyboardType
+        textField.autocapitalizationType = autocapitalizationType
+        textField.autocorrectionType = autocorrectionType
+
+        if shouldBecomeFirstResponder, !context.coordinator.didBecomeFirstResponder {
+            if textField.window != nil, textField.becomeFirstResponder() {
+                context.coordinator.didBecomeFirstResponder = true
+            }
+        } else if !shouldBecomeFirstResponder, context.coordinator.didBecomeFirstResponder {
+            context.coordinator.didBecomeFirstResponder = false
+        }
+    }
+
+    public func makeCoordinator() -> Coordinator {
+        Coordinator(self, maxLength: maxLength)
+    }
+
+    public final class Coordinator: NSObject {
+        var parent: TextFieldWithToolBar
+        var textField: UITextField?
+        let maxLength: Int?
+        var didBecomeFirstResponder = false
+        let decimalFormatter: NumberFormatter
+
+        init(_ parent: TextFieldWithToolBar, maxLength: Int?) {
+            self.parent = parent
+            self.maxLength = maxLength
+            decimalFormatter = NumberFormatter()
+            decimalFormatter.locale = Locale.current
+            decimalFormatter.numberStyle = .decimal
+        }
+
+        @objc fileprivate func clearText() {
+            parent.text = 0
+            textField?.text = ""
+        }
+
+        @objc fileprivate func editingDidBegin(_ textField: UITextField) {
+            DispatchQueue.main.async {
+                textField.moveCursorToEnd()
+            }
+        }
+    }
+}
+
+extension TextFieldWithToolBar.Coordinator: UITextFieldDelegate {
+    public func textField(
+        _ textField: UITextField,
+        shouldChangeCharactersIn range: NSRange,
+        replacementString string: String
+    ) -> 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,
+           let currentText = textField.text as NSString?
+        {
+            // Get the proposed new text
+            let proposedText = currentText.replacingCharacters(in: range, with: string)
+
+            // Try to convert proposed text to number
+            let number = parent.numberFormatter.number(from: proposedText) ?? decimalFormatter.number(from: proposedText)
+
+            // Update the binding value if conversion is successful
+            if let number = number {
+                parent.text = number.decimalValue
+            } else {
+                parent.text = 0
+            }
+        }
+
+        // Allow the change if it's a valid number or decimal separator
+        return isNumber || isDecimalSeparator && parent.allowDecimalSeparator
+    }
+
+    public func textFieldDidBeginEditing(_: UITextField) {
+        parent.textFieldDidBeginEditing?()
+    }
+}
+
+extension UITextField {
+    func moveCursorToEnd() {
+        dispatchPrecondition(condition: .onQueue(.main))
+        let newPosition = endOfDocument
+        selectedTextRange = textRange(from: newPosition, to: newPosition)
+    }
+}
+
+extension UIApplication {
+    func endEditing() {
+        sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
+    }
+}
+
+public struct TextFieldWithToolBarString: UIViewRepresentable {
+    @Binding var text: String
+    var placeholder: String
+    var textAlignment: NSTextAlignment = .right
+    var keyboardType: UIKeyboardType = .default
+    var autocapitalizationType: UITextAutocapitalizationType = .none
+    var autocorrectionType: UITextAutocorrectionType = .no
+    var shouldBecomeFirstResponder: Bool = false
+    var maxLength: Int? = nil
+    var isDismissible: Bool = true
+
+    public func makeUIView(context: Context) -> UITextField {
+        let textField = UITextField()
+        context.coordinator.textField = textField
+        textField.inputAccessoryView = isDismissible ? makeDoneToolbar(for: textField, context: context) : nil
+        textField.addTarget(context.coordinator, action: #selector(Coordinator.editingDidBegin), for: .editingDidBegin)
+        textField.delegate = context.coordinator
+        textField.text = text
+        textField.placeholder = placeholder
+        textField.textAlignment = textAlignment
+        textField.keyboardType = keyboardType
+        textField.autocapitalizationType = autocapitalizationType
+        textField.autocorrectionType = autocorrectionType
+        textField.adjustsFontSizeToFitWidth = true
+        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)
+        )
+
+        toolbar.items = [clearButton, flexibleSpace, doneButton]
+        toolbar.sizeToFit()
+        return toolbar
+    }
+
+    public func updateUIView(_ textField: UITextField, context: Context) {
+        if textField.text != text {
+            textField.text = text
+        }
+
+        textField.textAlignment = textAlignment
+        textField.keyboardType = keyboardType
+        textField.autocapitalizationType = autocapitalizationType
+        textField.autocorrectionType = autocorrectionType
+
+        if shouldBecomeFirstResponder, !context.coordinator.didBecomeFirstResponder {
+            if textField.window != nil, textField.becomeFirstResponder() {
+                context.coordinator.didBecomeFirstResponder = true
+            }
+        } else if !shouldBecomeFirstResponder, context.coordinator.didBecomeFirstResponder {
+            context.coordinator.didBecomeFirstResponder = false
+        }
+    }
+
+    public func makeCoordinator() -> Coordinator {
+        Coordinator(self, maxLength: maxLength)
+    }
+
+    public final class Coordinator: NSObject {
+        var parent: TextFieldWithToolBarString
+        var textField: UITextField?
+        let maxLength: Int?
+        var didBecomeFirstResponder = false
+
+        init(_ parent: TextFieldWithToolBarString, maxLength: Int?) {
+            self.parent = parent
+            self.maxLength = maxLength
+        }
+
+        @objc fileprivate func clearText() {
+            parent.text = ""
+            textField?.text = ""
+        }
+
+        @objc fileprivate func editingDidBegin(_ textField: UITextField) {
+            DispatchQueue.main.async {
+                textField.moveCursorToEnd()
+            }
+        }
+    }
+}
+
+extension TextFieldWithToolBarString.Coordinator: UITextFieldDelegate {
+    public func textField(
+        _ textField: UITextField,
+        shouldChangeCharactersIn range: NSRange,
+        replacementString string: String
+    ) -> Bool {
+        if let maxLength = parent.maxLength {
+            // Get the current text, including the proposed change
+            let currentText = textField.text ?? ""
+            let newLength = currentText.count + string.count - range.length
+            if newLength > maxLength {
+                return false
+            }
+        }
+
+        DispatchQueue.main.async {
+            if let textFieldText = textField.text as NSString? {
+                let newText = textFieldText.replacingCharacters(in: range, with: string)
+                self.parent.text = newText
+            }
+        }
+
+        return true
+    }
+}