Reputation: 119
I want to be able to edit a text field as the user types character by character. I want to be able to take a number field and as the user types:
If the user enters an invalid character then return the string with out the invalid character. I'm stuck on how to capture the TextField, edit it and return it formatted as I've shown.
I've gotten this far: (Not far :-) I've tried a number of solutions from here on StackOverflow but none seem to get me where I want to go.. onEditChange onChange etc.
struct SwiftUIView: View {
@State var amount: String = "0.00"
var body: some View {
TextField(
"Enter amount here ",
text: $amount
)
}
}
Upvotes: 0
Views: 3833
Reputation: 15
I ran into the same issue! Using the accepted answer as inspiration (thank you for posting!) I was able to come up with a less kludgy
solution.
import SwiftUI
struct Example: View {
@Binding var valueText: String
var body: some View {
TextField("Enter Value", text: $valueText)
.onChange(of: valueText, perform: { newValue in
valueText = editOnTheFly(input: newValue, leadingZeros: 0, decimalPlaces: 2)
})
}
func editOnTheFly(input: String, leadingZeros: Int, decimalPlaces: Int) -> String {
// Strip all non-numeric characters from the String
var result = input.replacingOccurrences(of: "[^0123456789]", with: "", options: .regularExpression)
// If the String can't be cast as a Double return ""
guard let resultAsDouble = Double(result) else {
return ""
}
let formatter = NumberFormatter()
formatter.minimumIntegerDigits = leadingZeros
formatter.minimumFractionDigits = decimalPlaces
formatter.maximumFractionDigits = decimalPlaces
// If `2` decimalPlaces past in, the divider would be 100.0
let dividerDecimal = pow(10.0, decimalPlaces)
let dividerDouble = NSDecimalNumber(decimal: dividerDecimal).doubleValue
// Turns an input of `42` into `0.42`
result = formatter.string(from: NSNumber(value: resultAsDouble/dividerDouble)) ?? ""
return result
}
}
Upvotes: 1
Reputation: 119
I suspect my request was too broad and maybe smacked of asking to do my programming for me. Such wasn't the case, but maybe I could have narrowed down the scope a little more.
Anyway, I was able to achieve my desired result. I think it's a giant KLUDGE but it was all I could come up with.
First my SwiftUI view:
struct SwiftUIView: View {
@State var someText: String = "0.00"
@State var oldText: String = ""
@State var amount: Double = 0.00
var body: some View {
VStack {
TextField("Enter some text here", text: $someText,
onCommit: {
print("someText = \(someText)")
self.amount = Double(someText)!
}
)
.onChange(of: someText, perform: { value in
if someText != oldText {
someText = editInputNumber(textIn: someText)
oldText = someText
}
}
)
Text("The amount entered was \(self.amount)")
}
}
}
Using onChange I ran into the problem that programmatically changing the bound field triggered the onChange a 2nd time. This is the biggest part of the Kludge where I have to test for the 2nd call to onChange.
Then the functions I created to respond to the user's input. This function is called from the view and it takes in the string and first calls a function to filter it down to only numbers. Eliminating any non-numerics accidentally entered
It takes the result and submits it to a formatting function and gets back the formatted string.
func editInputNumber(textIn: String) -> String
{
var fixedText = ""
var charactersWork = [Character]()
charactersWork = getNumericInputString(textIn: textIn)
fixedText = formatDecimalNumericString(charactersWork:
charactersWork)
return fixedText
}
Function to filter input string to only numbers.. It filters out any leading zeros from prior entries too.
func getNumericInputString(textIn: String) -> [Character]
{
var leadingZero = true
let charactersIn = Array(textIn)
var charactersOut = [Character]()
for i in 0..<charactersIn.count {
if charactersIn[i].isNumber {
if let number = Int(String(charactersIn[i]))
{
if number > 0
{
leadingZero = false
}
}
if !leadingZero {
charactersOut.append(charactersIn[i])
}
}
}
return charactersOut
}
After the string's been filtered down to a character array of only numbers this function formats it for output back to the view. If it is the first two numbers entered the function formats leading zeros on the value.
func formatDecimalNumericString(charactersWork: [Character]) -
> String
{
let number: String = "000"
var charactersOut = [Character]()
if charactersWork.count < 3
{
charactersOut = Array(number)
}
if charactersWork.count > 0
{
if charactersWork.count == 1
{
charactersOut[2] = charactersWork[0]
}
if charactersWork.count == 2
{
charactersOut[1] = charactersWork[0]
charactersOut[2] = charactersWork[1]
}
if charactersWork.count > 2
{
for i in 0..<charactersWork.count
{
charactersOut.append(charactersWork[i])
}
}
charactersOut.insert(".", at: (charactersOut.count - 2))
}
return String(charactersOut)
}
Upvotes: 0