Bevoid
Bevoid

Reputation: 119

Format text field to 2 decimal places without entering a decimal (SwiftUI)

In a text field, I'd like, when a user enters a number e.g. 12345, it gets formatted as 123.45. The user never needs to enter a decimal place, it just uses the 2 right most numbers as the decimal places. The field should only allow numbers too. This is for a SwiftUI project. Thanks in advance for any assistance.

Upvotes: 6

Views: 5058

Answers (2)

Sushant Gosavi
Sushant Gosavi

Reputation: 3825

With Swift UI the complete solution is

  1. TextField allow numeric value only
  2. Should accept only one comma (".")
  3. Restrict decimal point upto x decimal place

File NumbersOnlyViewModifier

import Foundation
import SwiftUI
import Combine
struct NumbersOnlyViewModifier: ViewModifier {
    
    @Binding var text: String
    var includeDecimal: Bool
    var digitAllowedAfterDecimal: Int = 1
    
    func body(content: Content) -> some View {
        content
            .keyboardType(includeDecimal ? .decimalPad : .numberPad)
            .onReceive(Just(text)) { newValue in
                var numbers = "0123456789"
                let decimalSeparator: String = Locale.current.decimalSeparator ?? "."
                if includeDecimal {
                    numbers += decimalSeparator
                }
                if newValue.components(separatedBy: decimalSeparator).count-1 > 1 {
                    let filtered = newValue
                    self.text = isValid(newValue: String(filtered.dropLast()), decimalSeparator: decimalSeparator)
                } else {
                    let filtered = newValue.filter { numbers.contains($0)}
                    if filtered != newValue {
                        self.text = isValid(newValue: filtered, decimalSeparator: decimalSeparator)
                    } else {
                        self.text = isValid(newValue: newValue, decimalSeparator: decimalSeparator)
                    }
                }
            }
    }
    
    private func isValid(newValue: String, decimalSeparator: String) -> String {
        guard includeDecimal, !text.isEmpty else { return newValue }
        let component = newValue.components(separatedBy: decimalSeparator)
        if component.count > 1 {
            guard let last = component.last else { return newValue }
            if last.count > digitAllowedAfterDecimal {
                let filtered = newValue
               return String(filtered.dropLast())
            }
        }
        return newValue
    }
}

File View+Extenstion

extension View {
    func numbersOnly(_ text: Binding<String>, includeDecimal: Bool = false) -> some View {
        self.modifier(NumbersOnlyViewModifier(text: text, includeDecimal: includeDecimal))
    }
} 

File ViewFile

 TextField("", text: $value,  onEditingChanged: { isEditing in
      self.isEditing = isEditing
   })

  .foregroundColor(Color.neutralGray900)
  .numbersOnly($value, includeDecimal: true)
  .font(.system(size: Constants.FontSizes.fontSize22))
  .multilineTextAlignment(.center)

Upvotes: 2

multitudes
multitudes

Reputation: 3495

Because there of a two way binding between what you enter and what is being shown in the TextField view it seems not possible to interpolate the displayed number entered. I would suggest a small hack:

  • create a ZStack with a TextField and a Text View superimposed.
  • the foreground font of the entered text in the TextField is clear or white .foregroundColor(.clear)
  • the keyboard is only number without decimal point: .keyboardType(.numberPad)
  • use .accentColor(.clear) to hide the cursor
  • the results are displayed in a Text View with formatting specifier: "%.2f"

It would look like

This is the code:

struct ContentView: View {
    @State private var enteredNumber = ""
    var enteredNumberFormatted: Double {
        return (Double(enteredNumber) ?? 0) / 100
    }
    var body: some View {

        Form {
            Section {
                ZStack(alignment: .leading) {
                    TextField("", text: $enteredNumber)
                        .keyboardType(.numberPad).foregroundColor(.clear)
                        .textFieldStyle(PlainTextFieldStyle())
                        .disableAutocorrection(true)
                        .accentColor(.clear)
                    Text("\(enteredNumberFormatted, specifier: "%.2f")")
                }
            }
        }
    }
}

Upvotes: 11

Related Questions