Deleting dollar sign in textfield with NumberFormatter causes value to be saved as 0

97 Views Asked by At

I am currently using a NumberFormatter for a textfield that is meant to accept a currency amount from the user.The text field starts with the "$0.00"

A problem I've been having is that if the dollar sign in the textfield is deleted, the value is saved as 0.00.

I believe that the problem has to due with how I set up the NumberFormatter, but I wasn't able to get much help from searches online.

    var formatter: NumberFormatter{
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        return formatter
    }

The Textfield:

TextField("Amount of regular paycheck", value: $amount, formatter: formatter)
                .keyboardType(.decimalPad)
                .background(Color.white)
                .textFieldStyle(.roundedBorder)
1

There are 1 best solutions below

1
adamek On

Storing currency as a double can lead to problems. Best practice is to store it as an integer as cents.

I use the following for my cash fields. The textfield state is stored as $dollarText. The currency value is stored as $cents.

If the user enters a properly formatted $1.23 string that the .currency format can recognize, then it converts that to an integer. If the user enters a double such as 1.23 then it converts that text to an integer and replaces the $dollarText with a properly formatted text string.

struct CashField : View {

    var thisLabel: String
    @Binding var cents: Int
    @State var dollarText: String
    @FocusState private var focusedField: Bool

    let cashFormat = getCashFormat()
    func getCashFormat() -> NumberFormatter {
        let thisCashFormat = NumberFormatter()
        thisCashFormat.numberStyle = NumberFormatter.Style.currency
        thisCashFormat.roundingMode = NumberFormatter.RoundingMode.halfEven
        thisCashFormat.maximumFractionDigits = 2
        return thisCashFormat
    }

    let basicFormat = getBasicFormat()
    func getBasicFormat() -> NumberFormatter {
        let thisBasicFormat = NumberFormatter()
        thisBasicFormat.numberStyle = NumberFormatter.Style.decimal
        thisBasicFormat.roundingMode = NumberFormatter.RoundingMode.halfEven
        thisBasicFormat.minimumFractionDigits = 0
        thisBasicFormat.maximumFractionDigits = 5
        return thisBasicFormat
    }

    init(thisLabel: String, cents: Binding<Int>) {
        self.thisLabel = thisLabel
        _cents = cents
        _dollarText = State(initialValue: cents.wrappedValue.cash())
    }
    
    var body: some View {
        TextField(text: $dollarText, prompt: nil) {
            Text(thisLabel)
        }
        .focused($focusedField)
        .padding(3)
        .overlay(
            RoundedRectangle(cornerRadius: 4)
                .stroke(.gray, lineWidth: 1)
        )
        .submitLabel(.done)
        .onChange(of: focusedField, perform: { newValue in
            if !newValue {
                // newValue is false when the field is no longer focused. Do your work on the values now.
                if let thisNumber = cashFormat.number(from: _dollarText.wrappedValue) {
                    _cents.wrappedValue = thisNumber.doubleValue.dollarsToCents()
                    _dollarText.wrappedValue = thisNumber.doubleValue.cash()
                } else if let thisNumber = basicFormat.number(from: _dollarText.wrappedValue) {
                    _cents.wrappedValue = thisNumber.doubleValue.dollarsToCents()
                    _dollarText.wrappedValue = thisNumber.doubleValue.cash()
                } else {
                    NSLog("Warning: Could not format \(_dollarText.wrappedValue)")
                }
            } else {
                NSLog("DEBUG: Start editing ")
            }
        })
    }
}

and a couple of extensions:

extension Double {
    func dollarsToCents() -> Int {
        let decimalDollars = NSDecimalNumber(value: self).decimalTwo()
        let decimalCents = decimalDollars.multiplying(byPowerOf10: 2)
        return decimalCents.intValue
    }
    func roundTwo() -> Double {
        return NSDecimalNumber(value: self).decimalTwo().doubleValue
    }
    func roundThree() -> Double {
        return NSDecimalNumber(value: self).decimalThree().doubleValue
    }
    func roundFive() -> Double {
        return NSDecimalNumber(value: self).decimalFive().doubleValue
    }
    func cash() -> String {
        if let cashString = cashFormat.string(from: NSNumber(value: self)) {
            return cashString
        } else {
            return "$$$ curious cash extension bug $$$"
        }
    }
    func stringPerCent() -> String {
        if let pcf = perCentFormat.string(from: NSNumber(value: self)) {
            return pcf
        } else {
            return "%%% curious stringPerCent extension bug %%%"
        }
    }
}

extension Int {
    
    func centsToDollars() -> NSDecimalNumber {
        let decimalCents = NSDecimalNumber(value: Int32(self))
        let decimalDollars = decimalCents.multiplying(byPowerOf10: -2)
        return decimalDollars
    }
    
    func centsToDoubleDollars() -> Double {
        return Double(self) / 100
    }
    
    func cash() -> String {
        // Assume integers = cents for this app.
        if let cashString = cashFormat.string(from: NSNumber(value: self.centsToDoubleDollars())) {
            return cashString
        } else {
            return "$$$ curious cash extension bug $$$"
        }
        
    }
}