Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to make it so that user enters currency only with numbers in SwiftUI textfield, while preserving the $ and the .?

Right now, I have the following:

private var currencyFormatter: NumberFormatter = {
    let f = NumberFormatter()
    // allow no currency symbol, extra digits, etc
    f.isLenient = true
    f.numberStyle = .currency
    return f
}()

TextField("Total", value: $totalInput, formatter: currencyFormatter)
    .font(.largeTitle)
    .padding()
    .background(Color.white)
    .foregroundColor(Color.black)
    .multilineTextAlignment(.center)

I want the textfield to start with $0.00 as a placeholder, but when the user starts entering, the first two inputs will be populated in the cents... so 5055 would progressively show as:

Step 1 (user hits 5): $0.05
Step 2 (user hits 0): $0.50
Step 3 (user hits 5): $5.05
Step 4 (user hits 5): $50.55

If the amount becomes greater than $999, then commas would be inserted.

How would one accomplish this? Right now my totalInput is type Double?.

like image 501
Rolando Avatar asked Oct 20 '25 14:10

Rolando


1 Answers

To create a currency field that allow the user to type an amount from right to left you would need an observable object (binding manager), a currency number formatter and observe every time the value changes using onChange method:

import SwiftUI

struct ContentView: View {
    @ObservedObject private var currencyManager = CurrencyManager(amount: 0)
    @ObservedObject private var currencyManagerUS = CurrencyManager(
        amount: 0,
        locale: .init(identifier: "en_US")
    )
    @ObservedObject private var currencyManagerUK = CurrencyManager(
        amount: 0,
        locale: .init(identifier: "en_UK")
    )
    @ObservedObject private var currencyManagerFR =  CurrencyManager(
        amount: 0,
        locale: .init(identifier: "fr_FR")
    )
    @ObservedObject private var currencyManagerBR =  CurrencyManager(
        amount: 100,
        maximum: 100,
        locale: .init(identifier: "pt_BR")
    )
    var body: some View {
        VStack(alignment: .trailing, spacing: 0) {
            Spacer()
            Group {
                Text("Locale currency")
                TextField(currencyManager.string, text: $currencyManager.string)
                    .keyboardType(.numberPad)
                    .multilineTextAlignment(.trailing)
                    .onChange(of: currencyManager.string, perform: currencyManager.valueChanged)
                Spacer()
            }
            Group {
                Text("American currency")
                TextField(currencyManagerUS.string, text: $currencyManagerUS.string)
                    .keyboardType(.numberPad)
                    .multilineTextAlignment(.trailing)
                    .onChange(of: currencyManagerUS.string, perform: currencyManagerUS.valueChanged)
                Spacer()
            }
            Group {
                Text("British currency")
                TextField(currencyManagerUK.string, text: $currencyManagerUK.string)
                    .keyboardType(.numberPad)
                    .multilineTextAlignment(.trailing)
                    .onChange(of: currencyManagerUK.string, perform: currencyManagerUK.valueChanged)
                Spacer()
            }
            Group {
                Text("French currency")
                TextField(currencyManagerFR.string, text: $currencyManagerFR.string)
                    .keyboardType(.numberPad)
                    .multilineTextAlignment(.trailing)
                    .onChange(of: currencyManagerFR.string, perform: currencyManagerFR.valueChanged)
                Spacer()
            }
            Group {
                Text("Brazilian currency")
                TextField(currencyManagerBR.string, text: $currencyManagerBR.string)
                    .keyboardType(.numberPad)
                    .multilineTextAlignment(.trailing)
                    .onChange(of: currencyManagerBR.string, perform: currencyManagerBR.valueChanged)
                
            }
            Spacer()
        }.padding(.trailing, 25)
    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        ContentView()
    }
}

class CurrencyManager: ObservableObject {
    @Published var string: String = ""
    private let formatter = NumberFormatter(numberStyle: .currency)
    private var maximum: Decimal = 999_999_999.99
    private var lastValue: String = ""
    
    init(amount: Decimal, maximum: Decimal = 999_999_999.99, locale: Locale = .current) {
        formatter.locale = locale
        self.string = formatter.string(for: amount) ?? "$0.00"
        self.lastValue = string
        self.maximum = maximum
    }
    
    func valueChanged(_ value: String) {
        let newValue = (value.decimal ?? .zero) / pow(10, formatter.maximumFractionDigits)
        if newValue > maximum {
            string = lastValue
        } else {
            string = formatter.string(for: newValue) ?? "$0.00"
            lastValue = string
        }
    }
}

extension NumberFormatter {
    convenience init(numberStyle: Style, locale: Locale = .current) {
        self.init()
        self.locale = locale
        self.numberStyle = numberStyle
    }
}

extension Character {
    var isDigit: Bool { "0"..."9" ~= self }
}

extension LosslessStringConvertible {
    var string: String { .init(self) }
}

extension StringProtocol where Self: RangeReplaceableCollection {
    var digits: Self { filter (\.isDigit) }
    var decimal: Decimal? { Decimal(string: digits.string) }
}

This is the SwiftUI equivalent to the custom CurrencyField I have implemented for UIKit.

like image 175
Leo Dabus Avatar answered Oct 22 '25 04:10

Leo Dabus



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!