From 99da4479d542243fa32e85591bac88e3f889fc7f Mon Sep 17 00:00:00 2001 From: polscm32 aka Marvout Date: Thu, 27 Jun 2024 01:40:00 +0200 Subject: [PATCH 1/3] Refactor current implementation of the DecimalTextField struct - fix constraint error - fix clear button not working as expected - make cursor right adjusted when tapping in the Textfield --- FreeAPS.xcodeproj/project.pbxproj | 8 +- .../AddCarbs/View/AddCarbsRootView.swift | 24 +-- .../View/AddTempTargetRootView.swift | 6 +- .../Modules/Bolus/View/BolusRootView.swift | 8 +- .../View/CalibrationsRootView.swift | 8 +- .../DataTable/View/DataTableRootView.swift | 16 +- .../FPUConfig/View/FPUConfigRootView.swift | 14 +- .../View/ManualTempBasalRootView.swift | 2 +- .../View/NightscoutConnectView.swift | 2 +- .../View/NotificationsConfigRootView.swift | 6 +- .../View/OverrideProfilesRootView.swift | 22 +-- .../View/PreferencesEditorRootView.swift | 8 +- .../View/PumpSettingsEditorRootView.swift | 6 +- .../StatConfig/View/StatConfigRootView.swift | 6 +- FreeAPS/Sources/Views/DecimalTextField.swift | 160 --------------- .../Sources/Views/TextFieldWithToolBar.swift | 187 ++++++++++++++++++ 16 files changed, 227 insertions(+), 256 deletions(-) delete mode 100644 FreeAPS/Sources/Views/DecimalTextField.swift create mode 100644 FreeAPS/Sources/Views/TextFieldWithToolBar.swift diff --git a/FreeAPS.xcodeproj/project.pbxproj b/FreeAPS.xcodeproj/project.pbxproj index e3c82b11c..e5723f794 100644 --- a/FreeAPS.xcodeproj/project.pbxproj +++ b/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 = ""; }; 3871F39B25ED892B0013ECB5 /* TempTarget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTarget.swift; sourceTree = ""; }; 3871F39E25ED895A0013ECB5 /* Decimal+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decimal+Extensions.swift"; sourceTree = ""; }; - 3883581B25EE79BB00E024B2 /* DecimalTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecimalTextField.swift; sourceTree = ""; }; + 3883581B25EE79BB00E024B2 /* TextFieldWithToolBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextFieldWithToolBar.swift; sourceTree = ""; }; 3883583325EEB38000E024B2 /* PumpSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpSettings.swift; sourceTree = ""; }; 388358C725EEF6D200E024B2 /* BasalProfileEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasalProfileEntry.swift; sourceTree = ""; }; 38887CCD25F5725200944304 /* IOBEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IOBEntry.swift; sourceTree = ""; }; @@ -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 */, diff --git a/FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift b/FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift index 2afe06961..d9c87458f 100644 --- a/FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift +++ b/FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift @@ -42,13 +42,7 @@ 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) @@ -268,25 +262,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) } } diff --git a/FreeAPS/Sources/Modules/AddTempTarget/View/AddTempTargetRootView.swift b/FreeAPS/Sources/Modules/AddTempTarget/View/AddTempTargetRootView.swift index 87a1772e9..a25bda924 100644 --- a/FreeAPS/Sources/Modules/AddTempTarget/View/AddTempTargetRootView.swift +++ b/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) } } diff --git a/FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift b/FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift index 51817c5db..167268599 100644 --- a/FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift +++ b/FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift @@ -65,13 +65,7 @@ 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) } } diff --git a/FreeAPS/Sources/Modules/Calibrations/View/CalibrationsRootView.swift b/FreeAPS/Sources/Modules/Calibrations/View/CalibrationsRootView.swift index 8bfaead27..fca7c4938 100644 --- a/FreeAPS/Sources/Modules/Calibrations/View/CalibrationsRootView.swift +++ b/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 { diff --git a/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift b/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift index 9739356c6..1e737533b 100644 --- a/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift +++ b/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift @@ -140,13 +140,7 @@ 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,13 +247,7 @@ 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) } } diff --git a/FreeAPS/Sources/Modules/FPUConfig/View/FPUConfigRootView.swift b/FreeAPS/Sources/Modules/FPUConfig/View/FPUConfigRootView.swift index d8df7d878..fd60d4f5f 100644 --- a/FreeAPS/Sources/Modules/FPUConfig/View/FPUConfigRootView.swift +++ b/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,22 @@ 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) } } diff --git a/FreeAPS/Sources/Modules/ManualTempBasal/View/ManualTempBasalRootView.swift b/FreeAPS/Sources/Modules/ManualTempBasal/View/ManualTempBasalRootView.swift index 4b230ec9c..33652a183 100644 --- a/FreeAPS/Sources/Modules/ManualTempBasal/View/ManualTempBasalRootView.swift +++ b/FreeAPS/Sources/Modules/ManualTempBasal/View/ManualTempBasalRootView.swift @@ -19,7 +19,7 @@ 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")) { diff --git a/FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConnectView.swift b/FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConnectView.swift index 7ae671ecd..cff0365c1 100644 --- a/FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConnectView.swift +++ b/FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConnectView.swift @@ -49,7 +49,7 @@ 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: "", numberFormatter: portFormater) } } header: { Text("Local glucose source") } } diff --git a/FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift b/FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift index 39b12dddc..e4d864e54 100644 --- a/FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift +++ b/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,7 @@ 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) } } diff --git a/FreeAPS/Sources/Modules/OverrideProfilesConfig/View/OverrideProfilesRootView.swift b/FreeAPS/Sources/Modules/OverrideProfilesConfig/View/OverrideProfilesRootView.swift index 7e8be9694..0ab35859e 100644 --- a/FreeAPS/Sources/Modules/OverrideProfilesConfig/View/OverrideProfilesRootView.swift +++ b/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) } } diff --git a/FreeAPS/Sources/Modules/PreferencesEditor/View/PreferencesEditorRootView.swift b/FreeAPS/Sources/Modules/PreferencesEditor/View/PreferencesEditorRootView.swift index 7ef673171..43375d418 100644 --- a/FreeAPS/Sources/Modules/PreferencesEditor/View/PreferencesEditorRootView.swift +++ b/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,11 +62,7 @@ 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( selection: $state.sections[sectionIndex].fields[fieldIndex].insulinCurveValue, diff --git a/FreeAPS/Sources/Modules/PumpSettingsEditor/View/PumpSettingsEditorRootView.swift b/FreeAPS/Sources/Modules/PumpSettingsEditor/View/PumpSettingsEditorRootView.swift index fcbd7826d..5f518813f 100644 --- a/FreeAPS/Sources/Modules/PumpSettingsEditor/View/PumpSettingsEditorRootView.swift +++ b/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) } } diff --git a/FreeAPS/Sources/Modules/StatConfig/View/StatConfigRootView.swift b/FreeAPS/Sources/Modules/StatConfig/View/StatConfigRootView.swift index fb12163d1..6d6938320 100644 --- a/FreeAPS/Sources/Modules/StatConfig/View/StatConfigRootView.swift +++ b/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) } } diff --git a/FreeAPS/Sources/Views/DecimalTextField.swift b/FreeAPS/Sources/Views/DecimalTextField.swift deleted file mode 100644 index 7b46a960f..000000000 --- a/FreeAPS/Sources/Views/DecimalTextField.swift +++ /dev/null @@ -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, - 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) - } -} diff --git a/FreeAPS/Sources/Views/TextFieldWithToolBar.swift b/FreeAPS/Sources/Views/TextFieldWithToolBar.swift new file mode 100644 index 000000000..1f6da9030 --- /dev/null +++ b/FreeAPS/Sources/Views/TextFieldWithToolBar.swift @@ -0,0 +1,187 @@ +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 + + public init( + text: Binding, + 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 + ) { + _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 + } + + 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, + 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 + } + + 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) + } +} From 49bc151a36a75cd5403b2e029f7146789e80358c Mon Sep 17 00:00:00 2001 From: polscm32 aka Marvout Date: Sun, 30 Jun 2024 21:32:59 +0200 Subject: [PATCH 2/3] Refactor notes Field as well --- .../AddCarbs/View/AddCarbsRootView.swift | 29 +++-- .../Modules/Bolus/View/BolusRootView.swift | 7 +- .../DataTable/View/DataTableRootView.swift | 14 +- .../FPUConfig/View/FPUConfigRootView.swift | 6 +- .../View/ManualTempBasalRootView.swift | 7 +- .../View/NotificationsConfigRootView.swift | 6 +- .../View/PreferencesEditorRootView.swift | 6 +- .../Sources/Views/TextFieldWithToolBar.swift | 121 ++++++++++++++++++ 8 files changed, 181 insertions(+), 15 deletions(-) diff --git a/FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift b/FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift index d9c87458f..f805e4ca3 100644 --- a/FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift +++ b/FreeAPS/Sources/Modules/AddCarbs/View/AddCarbsRootView.swift @@ -42,21 +42,34 @@ extension AddCarbs { HStack { Text("Carbs").fontWeight(.semibold) Spacer() - TextFieldWithToolBar(text: $state.carbs, placeholder: "0", shouldBecomeFirstResponder: true, numberFormatter: formatter) + TextFieldWithToolBar( + text: $state.carbs, + placeholder: "0", + shouldBecomeFirstResponder: true, + numberFormatter: formatter + ) Text(state.carbs > state.maxCarbs ? "⚠️" : "g").foregroundColor(.secondary) }.padding(.vertical) 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() diff --git a/FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift b/FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift index 167268599..a112b0a4b 100644 --- a/FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift +++ b/FreeAPS/Sources/Modules/Bolus/View/BolusRootView.swift @@ -65,7 +65,12 @@ extension Bolus { HStack { Text("Amount") Spacer() - TextFieldWithToolBar(text: $state.amount, placeholder: "0", shouldBecomeFirstResponder: true, numberFormatter: formatter) + TextFieldWithToolBar( + text: $state.amount, + placeholder: "0", + shouldBecomeFirstResponder: true, + numberFormatter: formatter + ) Text(state.amount > state.maxBolus ? "⚠️" : "U").foregroundColor(.secondary) } } diff --git a/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift b/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift index 1e737533b..4b211c44f 100644 --- a/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift +++ b/FreeAPS/Sources/Modules/DataTable/View/DataTableRootView.swift @@ -140,7 +140,12 @@ extension DataTable { Section { HStack { Text("New Glucose") - TextFieldWithToolBar(text: $state.manualGlucose, placeholder: " ... ", shouldBecomeFirstResponder: true, numberFormatter: glucoseFormatter) + TextFieldWithToolBar( + text: $state.manualGlucose, + placeholder: " ... ", + shouldBecomeFirstResponder: true, + numberFormatter: glucoseFormatter + ) Text(state.units.rawValue).foregroundStyle(.secondary) } } @@ -247,7 +252,12 @@ extension DataTable { HStack { Text("Amount") Spacer() - TextFieldWithToolBar(text: $state.externalInsulinAmount, placeholder: "0", shouldBecomeFirstResponder: true, numberFormatter: insulinFormatter) + TextFieldWithToolBar( + text: $state.externalInsulinAmount, + placeholder: "0", + shouldBecomeFirstResponder: true, + numberFormatter: insulinFormatter + ) Text("U").foregroundColor(.secondary) } } diff --git a/FreeAPS/Sources/Modules/FPUConfig/View/FPUConfigRootView.swift b/FreeAPS/Sources/Modules/FPUConfig/View/FPUConfigRootView.swift index fd60d4f5f..9272a7440 100644 --- a/FreeAPS/Sources/Modules/FPUConfig/View/FPUConfigRootView.swift +++ b/FreeAPS/Sources/Modules/FPUConfig/View/FPUConfigRootView.swift @@ -62,7 +62,11 @@ extension FPUConfig { HStack { Text("Override With A Factor Of ") Spacer() - TextFieldWithToolBar(text: $state.individualAdjustmentFactor, placeholder: "0.5", numberFormatter: conversionFormatter) + TextFieldWithToolBar( + text: $state.individualAdjustmentFactor, + placeholder: "0.5", + numberFormatter: conversionFormatter + ) } } diff --git a/FreeAPS/Sources/Modules/ManualTempBasal/View/ManualTempBasalRootView.swift b/FreeAPS/Sources/Modules/ManualTempBasal/View/ManualTempBasalRootView.swift index 33652a183..6679e4468 100644 --- a/FreeAPS/Sources/Modules/ManualTempBasal/View/ManualTempBasalRootView.swift +++ b/FreeAPS/Sources/Modules/ManualTempBasal/View/ManualTempBasalRootView.swift @@ -19,7 +19,12 @@ extension ManualTempBasal { HStack { Text("Amount") Spacer() - TextFieldWithToolBar(text: $state.rate, placeholder: "0", shouldBecomeFirstResponder: true, numberFormatter: formatter) + TextFieldWithToolBar( + text: $state.rate, + placeholder: "0", + shouldBecomeFirstResponder: true, + numberFormatter: formatter + ) Text("U/hr").foregroundColor(.secondary) } Picker(selection: $state.durationIndex, label: Text("Duration")) { diff --git a/FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift b/FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift index e4d864e54..cafd8acdc 100644 --- a/FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift +++ b/FreeAPS/Sources/Modules/NotificationsConfig/View/NotificationsConfigRootView.swift @@ -105,7 +105,11 @@ extension NotificationsConfig { HStack { Text("Carbs Required Threshold") Spacer() - TextFieldWithToolBar(text: $state.carbsRequiredThreshold, placeholder: "0", numberFormatter: carbsFormatter) + TextFieldWithToolBar( + text: $state.carbsRequiredThreshold, + placeholder: "0", + numberFormatter: carbsFormatter + ) Text("g").foregroundColor(.secondary) } } diff --git a/FreeAPS/Sources/Modules/PreferencesEditor/View/PreferencesEditorRootView.swift b/FreeAPS/Sources/Modules/PreferencesEditor/View/PreferencesEditorRootView.swift index 43375d418..b8e9d3d3c 100644 --- a/FreeAPS/Sources/Modules/PreferencesEditor/View/PreferencesEditorRootView.swift +++ b/FreeAPS/Sources/Modules/PreferencesEditor/View/PreferencesEditorRootView.swift @@ -62,7 +62,11 @@ extension PreferencesEditor { }) Text(field.displayName) } - TextFieldWithToolBar(text: self.$state.sections[sectionIndex].fields[fieldIndex].decimalValue, placeholder: "0", numberFormatter: formatter) + TextFieldWithToolBar( + text: self.$state.sections[sectionIndex].fields[fieldIndex].decimalValue, + placeholder: "0", + numberFormatter: formatter + ) case .insulinCurve: Picker( selection: $state.sections[sectionIndex].fields[fieldIndex].insulinCurveValue, diff --git a/FreeAPS/Sources/Views/TextFieldWithToolBar.swift b/FreeAPS/Sources/Views/TextFieldWithToolBar.swift index 1f6da9030..ca80c1588 100644 --- a/FreeAPS/Sources/Views/TextFieldWithToolBar.swift +++ b/FreeAPS/Sources/Views/TextFieldWithToolBar.swift @@ -185,3 +185,124 @@ extension UIApplication { 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 + } +} From c24bd7bb007cf6cc520a28506a4c1976eaa298ac Mon Sep 17 00:00:00 2001 From: polscm32 aka Marvout Date: Sat, 6 Jul 2024 11:24:19 +0200 Subject: [PATCH 3/3] fix NS Port Textfield showing a thousands place separator --- .../View/NightscoutConnectView.swift | 15 +++++++++++---- FreeAPS/Sources/Views/TextFieldWithToolBar.swift | 9 ++++++--- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConnectView.swift b/FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConnectView.swift index cff0365c1..9f69b8821 100644 --- a/FreeAPS/Sources/Modules/NightscoutConfig/View/NightscoutConnectView.swift +++ b/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") - TextFieldWithToolBar(text: $state.localPort, placeholder: "", numberFormatter: portFormater) + TextFieldWithToolBar( + text: $state.localPort, + placeholder: "", + keyboardType: .numberPad, + numberFormatter: portFormatter, + allowDecimalSeparator: false + ) } } header: { Text("Local glucose source") } } diff --git a/FreeAPS/Sources/Views/TextFieldWithToolBar.swift b/FreeAPS/Sources/Views/TextFieldWithToolBar.swift index ca80c1588..2095d8537 100644 --- a/FreeAPS/Sources/Views/TextFieldWithToolBar.swift +++ b/FreeAPS/Sources/Views/TextFieldWithToolBar.swift @@ -14,6 +14,7 @@ public struct TextFieldWithToolBar: UIViewRepresentable { var isDismissible: Bool var textFieldDidBeginEditing: (() -> Void)? var numberFormatter: NumberFormatter + var allowDecimalSeparator: Bool public init( text: Binding, @@ -27,7 +28,8 @@ public struct TextFieldWithToolBar: UIViewRepresentable { maxLength: Int? = nil, isDismissible: Bool = true, textFieldDidBeginEditing: (() -> Void)? = nil, - numberFormatter: NumberFormatter + numberFormatter: NumberFormatter, + allowDecimalSeparator: Bool = true ) { _text = text self.placeholder = placeholder @@ -42,6 +44,7 @@ public struct TextFieldWithToolBar: UIViewRepresentable { self.textFieldDidBeginEditing = textFieldDidBeginEditing self.numberFormatter = numberFormatter self.numberFormatter.numberStyle = .decimal + self.allowDecimalSeparator = allowDecimalSeparator } public func makeUIView(context: Context) -> UITextField { @@ -146,7 +149,7 @@ extension TextFieldWithToolBar.Coordinator: UITextFieldDelegate { 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, + if isNumber || isDecimalSeparator && parent.allowDecimalSeparator, let currentText = textField.text as NSString? { // Get the proposed new text @@ -164,7 +167,7 @@ extension TextFieldWithToolBar.Coordinator: UITextFieldDelegate { } // Allow the change if it's a valid number or decimal separator - return isNumber || isDecimalSeparator + return isNumber || isDecimalSeparator && parent.allowDecimalSeparator } public func textFieldDidBeginEditing(_: UITextField) {