|
|
@@ -18,6 +18,8 @@ public struct TextFieldWithToolBar: View {
|
|
|
var previousTextField: (() -> Void)?
|
|
|
var nextTextField: (() -> Void)?
|
|
|
var initialFocus: Bool
|
|
|
+ var unitsText: String?
|
|
|
+ var unitsTextColor: Color
|
|
|
|
|
|
@FocusState private var isFocused: Bool
|
|
|
@State private var localText: String = ""
|
|
|
@@ -40,7 +42,9 @@ public struct TextFieldWithToolBar: View {
|
|
|
showArrows: Bool = false,
|
|
|
previousTextField: (() -> Void)? = nil,
|
|
|
nextTextField: (() -> Void)? = nil,
|
|
|
- initialFocus: Bool = false
|
|
|
+ initialFocus: Bool = false,
|
|
|
+ unitsText: String? = nil,
|
|
|
+ unitsTextColor: Color = .secondary
|
|
|
) {
|
|
|
_text = text
|
|
|
self.placeholder = placeholder
|
|
|
@@ -59,150 +63,160 @@ public struct TextFieldWithToolBar: View {
|
|
|
self.previousTextField = previousTextField
|
|
|
self.nextTextField = nextTextField
|
|
|
self.initialFocus = initialFocus
|
|
|
+ self.unitsText = unitsText
|
|
|
+ self.unitsTextColor = unitsTextColor
|
|
|
}
|
|
|
|
|
|
public var body: some View {
|
|
|
- TextField(placeholder, text: $localText)
|
|
|
- .focused($isFocused)
|
|
|
- .multilineTextAlignment(textAlignment)
|
|
|
- .foregroundColor(textColor)
|
|
|
- .keyboardType(keyboardType)
|
|
|
- .toolbar {
|
|
|
- if isFocused {
|
|
|
- ToolbarItemGroup(placement: .keyboard) {
|
|
|
- Button(action: {
|
|
|
- localText = ""
|
|
|
- text = 0
|
|
|
- isZeroCleared = true // Mark as cleared to prevent showing "0"
|
|
|
- textDidChange?(0)
|
|
|
- }) {
|
|
|
- Image(systemName: "trash")
|
|
|
- }
|
|
|
-
|
|
|
- if showArrows {
|
|
|
- Button(action: { previousTextField?() }) {
|
|
|
- Image(systemName: "chevron.up")
|
|
|
+ HStack {
|
|
|
+ TextField(placeholder, text: $localText)
|
|
|
+ .focused($isFocused)
|
|
|
+ .multilineTextAlignment(textAlignment)
|
|
|
+ .foregroundColor(textColor)
|
|
|
+ .keyboardType(keyboardType)
|
|
|
+ .toolbar {
|
|
|
+ if isFocused {
|
|
|
+ ToolbarItemGroup(placement: .keyboard) {
|
|
|
+ Button(action: {
|
|
|
+ localText = ""
|
|
|
+ text = 0
|
|
|
+ isZeroCleared = true // Mark as cleared to prevent showing "0"
|
|
|
+ textDidChange?(0)
|
|
|
+ }) {
|
|
|
+ Image(systemName: "trash")
|
|
|
}
|
|
|
- Button(action: { nextTextField?() }) {
|
|
|
- Image(systemName: "chevron.down")
|
|
|
+
|
|
|
+ if showArrows {
|
|
|
+ Button(action: { previousTextField?() }) {
|
|
|
+ Image(systemName: "chevron.up")
|
|
|
+ }
|
|
|
+ Button(action: { nextTextField?() }) {
|
|
|
+ Image(systemName: "chevron.down")
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- Spacer()
|
|
|
+ Spacer()
|
|
|
|
|
|
- if isDismissible {
|
|
|
- Button(action: { isFocused = false }) {
|
|
|
- Image(systemName: "keyboard.chevron.compact.down")
|
|
|
+ if isDismissible {
|
|
|
+ Button(action: { isFocused = false }) {
|
|
|
+ Image(systemName: "keyboard.chevron.compact.down")
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
- .onChange(of: isFocused) { _, newValue in
|
|
|
- if newValue {
|
|
|
- textFieldDidBeginEditing?()
|
|
|
- // When gaining focus: if the value is zero and was previously cleared,
|
|
|
- // keep the text field empty to show placeholder instead of "0"
|
|
|
- if isZeroCleared, text == 0 {
|
|
|
- localText = ""
|
|
|
- }
|
|
|
- } else {
|
|
|
- // When losing focus: handle formatting and validation
|
|
|
- if localText.isEmpty {
|
|
|
- // If field is empty, maintain zero value but mark as cleared
|
|
|
- // so we can show placeholder instead of "0"
|
|
|
- text = 0
|
|
|
- isZeroCleared = true
|
|
|
- } else if let decimal = Decimal(string: localText, locale: numberFormatter.locale) {
|
|
|
- if decimal != 0 {
|
|
|
- // For non-zero values, format normally and update binding
|
|
|
- text = decimal
|
|
|
- localText = numberFormatter.string(from: decimal as NSNumber) ?? ""
|
|
|
- isZeroCleared = false
|
|
|
- } else {
|
|
|
- // If user explicitly entered zero, store the value but
|
|
|
- // keep display empty to show placeholder
|
|
|
- text = 0
|
|
|
+ .onChange(of: isFocused) { _, newValue in
|
|
|
+ if newValue {
|
|
|
+ textFieldDidBeginEditing?()
|
|
|
+ // When gaining focus: if the value is zero and was previously cleared,
|
|
|
+ // keep the text field empty to show placeholder instead of "0"
|
|
|
+ if isZeroCleared, text == 0 {
|
|
|
localText = ""
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // When losing focus: handle formatting and validation
|
|
|
+ if localText.isEmpty {
|
|
|
+ // If field is empty, maintain zero value but mark as cleared
|
|
|
+ // so we can show placeholder instead of "0"
|
|
|
+ text = 0
|
|
|
isZeroCleared = true
|
|
|
+ } else if let decimal = Decimal(string: localText, locale: numberFormatter.locale) {
|
|
|
+ if decimal != 0 {
|
|
|
+ // For non-zero values, format normally and update binding
|
|
|
+ text = decimal
|
|
|
+ localText = numberFormatter.string(from: decimal as NSNumber) ?? ""
|
|
|
+ isZeroCleared = false
|
|
|
+ } else {
|
|
|
+ // If user explicitly entered zero, store the value but
|
|
|
+ // keep display empty to show placeholder
|
|
|
+ text = 0
|
|
|
+ localText = ""
|
|
|
+ isZeroCleared = true
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
- .onChange(of: localText) { oldValue, newValue in
|
|
|
- // Reset zero-cleared state as soon as user starts typing anything
|
|
|
- if !newValue.isEmpty {
|
|
|
- isZeroCleared = false
|
|
|
- }
|
|
|
-
|
|
|
- // Special handling for backspace operations to maintain decimal format
|
|
|
- if oldValue.count == newValue.count + 1 {
|
|
|
- let decimalSeparator = numberFormatter.decimalSeparator ?? "."
|
|
|
-
|
|
|
- // Special case: When backspacing to leave only a decimal point
|
|
|
- // e.g., "10.1" -> "10." - Keep decimal separator without adding trailing zero
|
|
|
- if newValue.hasSuffix(decimalSeparator) {
|
|
|
- if let decimal = Decimal(string: newValue + "0", locale: numberFormatter.locale) {
|
|
|
- text = decimal
|
|
|
- textDidChange?(decimal)
|
|
|
- }
|
|
|
- return
|
|
|
+ .onChange(of: localText) { oldValue, newValue in
|
|
|
+ // Reset zero-cleared state as soon as user starts typing anything
|
|
|
+ if !newValue.isEmpty {
|
|
|
+ isZeroCleared = false
|
|
|
}
|
|
|
|
|
|
- // Special case: When backspacing the last digit after a decimal point
|
|
|
- // e.g., "10.0" -> "10." - Ensure we keep proper decimal format
|
|
|
- if oldValue.contains(decimalSeparator), newValue.contains(decimalSeparator) {
|
|
|
- let oldParts = oldValue.components(separatedBy: decimalSeparator)
|
|
|
- let newParts = newValue.components(separatedBy: decimalSeparator)
|
|
|
-
|
|
|
- // Check if we've removed the last digit after decimal point
|
|
|
- if oldParts.count > 1, newParts.count > 1,
|
|
|
- oldParts[1].count == 1, newParts[1].isEmpty
|
|
|
- {
|
|
|
- // Keep proper decimal format by adding trailing zero
|
|
|
- localText = newParts[0] + decimalSeparator + "0"
|
|
|
+ // Special handling for backspace operations to maintain decimal format
|
|
|
+ if oldValue.count == newValue.count + 1 {
|
|
|
+ let decimalSeparator = numberFormatter.decimalSeparator ?? "."
|
|
|
|
|
|
- if let decimal = Decimal(string: localText, locale: numberFormatter.locale) {
|
|
|
+ // Special case: When backspacing to leave only a decimal point
|
|
|
+ // e.g., "10.1" -> "10." - Keep decimal separator without adding trailing zero
|
|
|
+ if newValue.hasSuffix(decimalSeparator) {
|
|
|
+ if let decimal = Decimal(string: newValue + "0", locale: numberFormatter.locale) {
|
|
|
text = decimal
|
|
|
textDidChange?(decimal)
|
|
|
}
|
|
|
return
|
|
|
}
|
|
|
+
|
|
|
+ // Special case: When backspacing the last digit after a decimal point
|
|
|
+ // e.g., "10.0" -> "10." - Ensure we keep proper decimal format
|
|
|
+ if oldValue.contains(decimalSeparator), newValue.contains(decimalSeparator) {
|
|
|
+ let oldParts = oldValue.components(separatedBy: decimalSeparator)
|
|
|
+ let newParts = newValue.components(separatedBy: decimalSeparator)
|
|
|
+
|
|
|
+ // Check if we've removed the last digit after decimal point
|
|
|
+ if oldParts.count > 1, newParts.count > 1,
|
|
|
+ oldParts[1].count == 1, newParts[1].isEmpty
|
|
|
+ {
|
|
|
+ // Keep proper decimal format by adding trailing zero
|
|
|
+ localText = newParts[0] + decimalSeparator + "0"
|
|
|
+
|
|
|
+ if let decimal = Decimal(string: localText, locale: numberFormatter.locale) {
|
|
|
+ text = decimal
|
|
|
+ textDidChange?(decimal)
|
|
|
+ }
|
|
|
+ return
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
- }
|
|
|
|
|
|
- // Process normal text input changes
|
|
|
- handleTextChange(oldValue, newValue)
|
|
|
- }
|
|
|
- .onChange(of: text) { oldValue, newValue in
|
|
|
- // Handle external changes to the text binding
|
|
|
- // (changes not initiated by typing, like programmatic changes)
|
|
|
- if oldValue != newValue,
|
|
|
- Decimal(string: localText, locale: numberFormatter.locale) != newValue
|
|
|
- {
|
|
|
- if newValue == 0, isZeroCleared {
|
|
|
- // If value is zero and field was cleared, keep display empty to show placeholder
|
|
|
- localText = ""
|
|
|
- } else {
|
|
|
- // Otherwise format and display the new value
|
|
|
- localText = numberFormatter.string(from: newValue as NSNumber) ?? ""
|
|
|
- isZeroCleared = false
|
|
|
+ // Process normal text input changes
|
|
|
+ handleTextChange(oldValue, newValue)
|
|
|
+ }
|
|
|
+ .onChange(of: text) { oldValue, newValue in
|
|
|
+ // Handle external changes to the text binding
|
|
|
+ // (changes not initiated by typing, like programmatic changes)
|
|
|
+ if oldValue != newValue,
|
|
|
+ Decimal(string: localText, locale: numberFormatter.locale) != newValue
|
|
|
+ {
|
|
|
+ if newValue == 0, isZeroCleared {
|
|
|
+ // If value is zero and field was cleared, keep display empty to show placeholder
|
|
|
+ localText = ""
|
|
|
+ } else {
|
|
|
+ // Otherwise format and display the new value
|
|
|
+ localText = numberFormatter.string(from: newValue as NSNumber) ?? ""
|
|
|
+ isZeroCleared = false
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
- }
|
|
|
- .onAppear {
|
|
|
- if text != 0 {
|
|
|
- // Initialize with formatted non-zero value
|
|
|
- localText = numberFormatter.string(from: text as NSNumber) ?? ""
|
|
|
- isZeroCleared = false
|
|
|
- } else {
|
|
|
- // For zero values, start with empty field to show placeholder
|
|
|
- localText = ""
|
|
|
- isZeroCleared = true
|
|
|
+ .onAppear {
|
|
|
+ if text != 0 {
|
|
|
+ // Initialize with formatted non-zero value
|
|
|
+ localText = numberFormatter.string(from: text as NSNumber) ?? ""
|
|
|
+ isZeroCleared = false
|
|
|
+ } else {
|
|
|
+ // For zero values, start with empty field to show placeholder
|
|
|
+ localText = ""
|
|
|
+ isZeroCleared = true
|
|
|
+ }
|
|
|
+ // Set initial focus if requested
|
|
|
+ isFocused = initialFocus
|
|
|
}
|
|
|
- // Set initial focus if requested
|
|
|
- isFocused = initialFocus
|
|
|
+ if unitsText != nil {
|
|
|
+ Text(unitsText ?? "").foregroundColor(unitsTextColor)
|
|
|
+ .onTapGesture {
|
|
|
+ isFocused = true
|
|
|
+ }
|
|
|
}
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private func handleTextChange(_ oldValue: String, _ newValue: String) {
|