UPGRADE YOUR SKILLS: Learn advanced Swift and SwiftUI on Hacking with Swift+! >>

How to limit TextField to a maximum of two decimal places

Forums > Swift

I have a Form with a Text Field for entering a dollar amount (401k Balance). Currently the user can enter as many digits to the right of the decimal place as they want. I would like to restrict their ability to enter numbers beyond the hundreth place (beyond two decimal places).

Section(header: Text("Current 401k Balance:")
                    .foregroundColor(colorScheme == .dark ? Color.white: Color.black)
                    .font(.body)
                    .bold()) {
                    TextField("Current 401k Balance", value: $vm.userData.startBalance401k, format: .currency(code: "USD"))
                        .focused($amountIsFocused)
                        .keyboardType(.decimalPad)
                        .toolbar {
                            ToolbarItemGroup(placement: .keyboard) {
                                Spacer()
                                Button {
                                    amountIsFocused = false
                                } label: {
                                    Text("Done")
                                }
                            }
                        }
                }

2      

You could make the keyboard disappear as soon as the user has entered two decimal places. To do so, you would need to observe the vm.userData.startBalance401k value, convert it into a string and count the digits after the decimal point.

    @State private var startBalance401k = 0.0 // I've decided to use a Double, since I don't know the type of startBalance401k

Add this after .keyboardType(.decimalPad):

.onChange(of: startBalance401k) { _ in
                        let stringStartBalance401k = String(startBalance401k) // Convert startBalance401k into a string
                        let components = stringStartBalance401k.components(separatedBy: ".") // Extract two other strings from stringStartBalance401k, the first one holds all the digits to the left of the decimal place and the second one holds all the digits to the right of the decimal place.

                        if components.count == 2, components[1].count >= 2 { // Check if components contains two elements, if it does, this means that the user has entered a decimal place. If components[1], i.e. the number of digits to the right of decimal place, is equal or greater than 2 you can react. 
                            amountIsFocused = false // Hide the keyboard
                        }
                    }

2      

As a possibility. This will allow you to enter maximum 2 digits after decimal place. Data is considered as Double... but maybe it is different in your model.

struct ContentView: View {
    @EnvironmentObject var vm: ViewModel // place vm in environment
    @State var amount: String // string to be passed via the view that calls it
    @FocusState var amountIsFocused: Bool

    var body: some View {
      NavigationStack {
          Section {
              TextField("Current 401k Balance", text: $amount)
                  .padding()
                  .focused($amountIsFocused)
                  .keyboardType(.decimalPad)
                    // find a decimal place and separate digits on left and right side
                    // NOTE: need to check how it works with "," decimal separator
                  .onChange(of: amount) { _ in
                      let amountString = String(amount)
                      let start = amountString.startIndex
                      let findDot = amountString.firstIndex(of: ".")
                      if let dotIndex = findDot {
                          let leftSide = String(amountString[start..<dotIndex])
                          // by prefixing string you do not allow it to be more than 2 decimal places
                          // .prefix(3) because decimal point is also counted
                          let rightSide = String(amountString[dotIndex...]).prefix(3)
                          // update the amount visible on the text field
                          amount = leftSide + rightSide
                      }
                  }
                  .onChange(of: vm.data, perform: { _ in
                      // once the amount in model is updated after pressing "Done" button
                      // you refresh TextField to show currency symbol as well
                      amount = vm.data.formatted(.currency(code: "USD"))
                  })
                  .onAppear {
                      // TextField is focused once the view appears
                      // Remove if no such functionality is required
                      amountIsFocused = true
                  }
                  .toolbar {
                      ToolbarItemGroup(placement: .keyboard) {
                          Spacer()
                          Button("Done") {
                              amountIsFocused = false
                              // update the amount in model as you are working with string here
                              vm.data = Double(amount) ?? 0
                              print("\(vm.data)")
                          }
                      }
                  }
          } header: {
              Text("Current 401k Balance:")
                  .font(.body)
                  .bold()
          }
        }
    }
}

2      

@ygeras, your onChange handler can be simplified (and localized) to this:

let sep = Locale.current.decimalSeparator ?? "."
var parts = amount.split(separator: sep.first!)
if parts.count > 1, let lastItem = parts.popLast() {
    parts.append(lastItem.prefix(2))
    amount = parts.joined(separator: sep)
}

@comeandtakeit99, I would suggest trying a different approach. Some ideas:

  1. Code like this works:
struct LimitedCurrencyField: View {
    @State private var amount: Double = 0
    @State private var label = ""

    let currencyFormatter: NumberFormatter = {
        let fmt = NumberFormatter()
        fmt.numberStyle = .currency
        fmt.minimumFractionDigits = 2
        fmt.maximumFractionDigits = 2
        return fmt
    }()

    var body: some View {
        VStack {
            TextField("Current 401k Balance", value: $amount, formatter: currencyFormatter)
                .keyboardType(.decimalPad)
            TextField("Label", text: $label)
        }
    }
}

But has the proviso that you have to have another TextField for the user to click into for the formatting to take effect. That's because the changes don't take effect until you commit the changes, which is usually done by hitting Enter or moving to another TextField. And since .decimalPad doesn't have an Enter key...

  1. This works as well, with the same proviso as #1:
struct LimitedCurrencyField: View {
    @State private var amount: Double = 0
    @State private var label = ""

    var body: some View {
        VStack {
            TextField("Current 401k Balance", value: $amount, format: .currency(code: "USD"))
                .keyboardType(.decimalPad)
                .onChange(of: amount) {
                    let amountString = String(format: "%.2f", $0)
                    amount = Double(amountString)!
                }
            TextField("Label", text: $label)
        }
    }
}
  1. Use a solution like this one presented by Leo Dabus at StackOverflow. I like this one because it completely eliminates the need to worry about how many digits are after the decimal point as the user types.

3      

@roosterboy In solution 2 the label not updated! Did you mean to assign the String(format: "%.2f", $0) to label

As a quick note with the TextField for label give the user chance to change the label directly if use .constant this can not be change by entering the TextField but will still update

struct LimitedCurrencyField: View {
    @State private var amount: Double = 0
    @State private var label = ""

    var body: some View {
        VStack {
            TextField("Current 401k Balance", value: $amount, format: .currency(code: "USD"))
                .keyboardType(.decimalPad)
                .onChange(of: amount) {
                    label = String(format: "%.2f", $0)
                    amount = Double(label)!
                }
            TextField("Label", text: .constant(label))
        }
    }
}

2      

The label TextField is just put in there to have something the user can click into to commit their change to the amount TextField, as per the proviso I mentioned. There would be no real point to setting the label TextField to String(format: "%.2f", $0) since that info is already being displayed in the amount TextField. It was for purposes of illustrating the proviso but could have been any kind of information, as long as it was a different TextField for the user to move to.

2      

On Sodapool's suggestion, while that is a way to accomplish it, it will not (reliably, anyway) work if the number is pasted.

I use NumberFormatter as roosterboy suggested. Note that one NumberFormatter instance can be reused over and over.

2      

Ahh @roosterboy. I get what you saying.

2      

Hacking with Swift is sponsored by RevenueCat

SPONSORED Take the pain out of configuring and testing your paywalls. RevenueCat's Paywalls allow you to remotely configure your entire paywall view without any code changes or app updates.

Learn more here

Sponsor Hacking with Swift and reach the world's largest Swift community!

Archived topic

This topic has been closed due to inactivity, so you can't reply. Please create a new topic if you need to.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.