rpetruzz
rpetruzz

Reputation: 119

Editing a SwiftUI TextField on the fly

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:

  1. Filter out non-numeric characters and return back only the characters already entered that are valid.
  2. Format the number such that when the user enters they get the number in reverse. ie 120.36 would appear as: 0.01 0.12 1.20 12.03 120.36

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

Answers (2)

maxrgnt
maxrgnt

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

rpetruzz
rpetruzz

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

Related Questions