How to input currency format on a text field (from right to left) using Swift?
Asked Answered
H

9

69

I have a number let’s say 0.00.

  • When the user taps 1. We should have 0.01
  • When the user taps 2. We should display 0.12
  • When the user taps 3. We should display 1.23
  • When the user taps 4. We should display 12.34

How can I do that with Swift?

Hellenize answered 21/4, 2015 at 20:57 Comment(6)
For the readability of the SO posts, please do not include ‘bolo’, ‘thanks in advance’ or other text that does not help your question in your question.Crest
@Crest sorry ! I will fix thatHellenize
Your last number should be 12.34, shouldn't it?Gonfanon
@ThomasKilian yes you are rigthHellenize
Has anyone ported this over to SwiftUI yet!?Steamer
@Steamer I have at github.com/youjinp/SwiftUIKitStegall
D
101

For Swift 3. Input currency format on a text field (from right to left)

override func viewDidLoad() {
    super.viewDidLoad()

    textField.addTarget(self, action: #selector(myTextFieldDidChange), for: .editingChanged)
}

@objc func myTextFieldDidChange(_ textField: UITextField) {

    if let amountString = textField.text?.currencyInputFormatting() {
        textField.text = amountString
    }
}

extension String {

    // formatting text for currency textField
    func currencyInputFormatting() -> String {
    
        var number: NSNumber!
        let formatter = NumberFormatter()
        formatter.numberStyle = .currencyAccounting
        formatter.currencySymbol = "$"
        formatter.maximumFractionDigits = 2
        formatter.minimumFractionDigits = 2
    
        var amountWithPrefix = self
    
        // remove from String: "$", ".", ","
        let regex = try! NSRegularExpression(pattern: "[^0-9]", options: .caseInsensitive)
        amountWithPrefix = regex.stringByReplacingMatches(in: amountWithPrefix, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, self.count), withTemplate: "")
    
        let double = (amountWithPrefix as NSString).doubleValue
        number = NSNumber(value: (double / 100))
    
        // if first number is 0 or all numbers were deleted
        guard number != 0 as NSNumber else {
            return ""
        }
    
        return formatter.string(from: number)!
    }
}
Diminution answered 7/11, 2016 at 16:2 Comment(11)
You could find a simple example here github.com/vivatum/Currency_Format_from_left_to_rightBacker
this one is pretty cool and works fine, but Is there a way to delete the last entered digit using the delete button on the keyboard? At the moment nothing is happening when the button is pressed. Thanks for your help!Symposium
@Symposium thanks for your comment! Did you try my example of entire code (link above)? I've checked it one more time right now. And I've not encountered any troubles with the case "to delete the last entered digit using the delete button on the keyboard".Backer
its strange but your example isn't working and I tried another solution but the delete button does nothing, so I guess I have to investigate further...Symposium
@Symposium try to do iPhone Simulator > Reset Content and SettingsBacker
That was the solution! Now it's working! Thanks a lotSymposium
This is not working if you passed direct value to UITextField instead of keyboard type. !!Collywobbles
This answer has a few problems. This answer assumes a fixed currency format with a fixed currency symbol. Why? Only some countries use the $ symbol. Not all countries use two decimal places for currency.Livvi
Actually this is not working when the currency is displayed on the right side of the amount (it depends on the locale phone setting) - then you can't delete the valueTandy
I need the same example in Jquery or JavascriptOeflein
This shouldn't be used if for no other reason than that it stores a currency as a Double brieflyRenz
H
46

You can create a currency text field subclassing UITextField. Add a target for UIControlEvents .editingChanged. Add a selector method to filter the digits from your textfield string. After filtering all non digits from your string you can format again your number using NumberFormatter as follow:

Xcode 11.5 • Swift 5.2 or later

import UIKit

class CurrencyField: UITextField {
    var decimal: Decimal { string.decimal / pow(10, Formatter.currency.maximumFractionDigits) }
    var maximum: Decimal = 999_999_999.99
    private var lastValue: String?
    var locale: Locale = .current {
        didSet {
            Formatter.currency.locale = locale
            sendActions(for: .editingChanged)
        }
    }
    override func willMove(toSuperview newSuperview: UIView?) {
        // you can make it a fixed locale currency if needed
        // self.locale = Locale(identifier: "pt_BR") // or "en_US", "fr_FR", etc
        Formatter.currency.locale = locale
        addTarget(self, action: #selector(editingChanged), for: .editingChanged)
        keyboardType = .numberPad
        textAlignment = .right
        sendActions(for: .editingChanged)
    }
    override func deleteBackward() {
        text = string.digits.dropLast().string
        // manually send the editingChanged event
        sendActions(for: .editingChanged)
    }
    @objc func editingChanged() {
        guard decimal <= maximum else {
            text = lastValue
            return
        }
        text = decimal.currency
        lastValue = text
    }
}

extension CurrencyField {
    var doubleValue: Double { (decimal as NSDecimalNumber).doubleValue }
}

extension UITextField {
     var string: String { text ?? "" }
}

extension NumberFormatter {
    convenience init(numberStyle: Style) {
        self.init()
        self.numberStyle = numberStyle
    }
}

private extension Formatter {
    static let currency: NumberFormatter = .init(numberStyle: .currency)
}

extension StringProtocol where Self: RangeReplaceableCollection {
    var digits: Self { filter (\.isWholeNumber) }
}

extension String {
    var decimal: Decimal { Decimal(string: digits) ?? 0 }
}

extension Decimal {
    var currency: String { Formatter.currency.string(for: self) ?? "" }
}

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

View Controller

class ViewController: UIViewController {

    @IBOutlet weak var currencyField: CurrencyField!
    override func viewDidLoad() {
        super.viewDidLoad()
        currencyField.addTarget(self, action: #selector(currencyFieldChanged), for: .editingChanged)
        currencyField.locale = Locale(identifier: "pt_BR") // or "en_US", "fr_FR", etc
    }
    @objc func currencyFieldChanged() {
        print("currencyField:",currencyField.text!)
        print("decimal:", currencyField.decimal)
        print("doubleValue:",(currencyField.decimal as NSDecimalNumber).doubleValue, terminator: "\n\n")
    }
}

Sample project


SwiftUI version of this post here

Helmet answered 21/4, 2015 at 21:35 Comment(6)
You wrote the line: let cleanText = String(Array(sender.text).map{String($0)}.filter{ $0.toInt() != nil }.map{Character($0)} ) as NSString. Why make such a complex statement? Break that into 3 or 4 pieces with temporary variables. It makes it much, much easier to read, debug, and maintain, and the compiler optimizes away the temporary variables in the release build.Pola
This is great and simple! But I work on an finance app and let the user change his currency, so I somehow need to set this on the textfield. I tried to set the locale with Formatter.currency.locale = Locale(identifier: "es_CL") text = Formatter.currency.string(from:(Double(string.numbers.integer) / 100) as NSNumber) But with this locale for example the textfield doesn't work anymore? Do you know why?Lyublin
@LeoDabus Posted question here: #41712044 I guess the problem is not how to set the locale, it's the locale itself, because the NumberFormatter doesn't but out decimals for this localeLyublin
@LeoDabus : Any Objective-c Solution pleaseOctastyle
Be very careful here; this doesn't work correctly if the cursor is not at then end of the field. If you're going to do something that replaces the text string, you have to factor in the UITextPosition/UITextRange.Selfcongratulation
@Selfcongratulation actually my personal implementation doesnt allow you to move the carretHelmet
P
9

I started with Leo Dabus' answer (which didn't work out of the box for me) and in the process of trying to simplify and make it work ended up with this, which I think is pretty lean & clean if I do say so myself 😎

class CurrencyTextField: UITextField {

    /// The numbers that have been entered in the text field
    private var enteredNumbers = ""

    private var didBackspace = false

    var locale: Locale = .current

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }

    private func commonInit() {
        addTarget(self, action: #selector(editingChanged), for: .editingChanged)
    }

    override func deleteBackward() {
        enteredNumbers = String(enteredNumbers.dropLast())
        text = enteredNumbers.asCurrency(locale: locale)
        // Call super so that the .editingChanged event gets fired, but we need to handle it differently, so we set the `didBackspace` flag first
        didBackspace = true
        super.deleteBackward()
    }

    @objc func editingChanged() {
        defer {
            didBackspace = false
            text = enteredNumbers.asCurrency(locale: locale)
        }

        guard didBackspace == false else { return }

        if let lastEnteredCharacter = text?.last, lastEnteredCharacter.isNumber {
            enteredNumbers.append(lastEnteredCharacter)
        }
    }
}

private extension Formatter {
    static let currency: NumberFormatter = {
        let formatter = NumberFormatter()
        formatter.numberStyle = .currency
        return formatter
    }()
}

private extension String {
    func asCurrency(locale: Locale) -> String? {
        Formatter.currency.locale = locale
        if self.isEmpty {
            return Formatter.currency.string(from: NSNumber(value: 0))
        } else {
            return Formatter.currency.string(from: NSNumber(value: (Double(self) ?? 0) / 100))
        }
    }
}
Punchdrunk answered 26/3, 2020 at 0:48 Comment(4)
how do you use it in a uitextfield??Dennison
@Dennison you'd make the text field in your view controller a CurrencyTextField instead of a UITextFieldPunchdrunk
Not all currencies have 2 fractional digits. Note that isNumber would allow fractional characters like “⅚” as well.Helmet
why not simply enteredNumbers.popLast()?Helmet
G
1

Try this piece of code:

struct DotNum {
  private var fraction:String = ""
  private var intval:String = ""
  init() {}
  mutating func enter(s:String) {
    if count(fraction) < 2 {
      fraction = s + fraction
    } else {
      intval = s + intval
    }
  }
  private var sFract:String {
    if count(fraction) == 0 { return "00" }
    if count(fraction) == 1 { return "0\(fraction)" }
    return fraction
  }
  var stringVal:String {
    if intval == ""  { return "0.\(sFract)" }
    return "\(intval).\(sFract)"
  }
}
var val = DotNum()
val.enter("1")
val.stringVal
val.enter("2")
val.stringVal
val.enter("3")
val.stringVal
val.enter("4")
val.stringVal
Gonfanon answered 21/4, 2015 at 21:16 Comment(1)
Best answer with no dependencies to any framework but pure Swift.Ocher
H
1

My final code thanks for your help

extension Double {
            var twoDigits: Double {
                let nf = NSNumberFormatter()
                nf.numberStyle = NSNumberFormatterStyle.DecimalStyle
                nf.minimumFractionDigits = 2
                nf.maximumFractionDigits = 2
                return self
            }
    }
    var cleanText:String!
            let number:String = sender.currentTitle as String!
            if(amountDisplay.text != nil)
            {
                cleanText = String(Array(amountDisplay.text!).map{String($0)}.filter{ $0.toInt() != nil }.map{Character($0)} ) as String
                cleanText = cleanText + number
            }else{
                cleanText = number
            }

            amount = (Double(cleanText.toInt()!) / 100).twoDigits
            formatter.locale = NSLocale(localeIdentifier: currencies[current_currency_index])
            amountDisplay.text = "\(formatter.stringFromNumber(amount!)!)"
Hellenize answered 22/4, 2015 at 4:59 Comment(2)
have you had a chance to convert this to SwiftUI?Steamer
@Steamer https://mcmap.net/q/244588/-how-to-make-it-so-that-user-enters-currency-only-with-numbers-in-swiftui-textfield-while-preserving-the-and-theHelmet
R
1

After a lot of trial and error with the suggested answers, I found a pretty straight forward solution:

The setup for the textField needs to be called in your view's setup.

In the switch statement, if the user puts in a number between 0 and 9, the number is added to the previous string value. The default case covers the backspace button and removes the last character from the string.

The locale for the numberFormatter is set to current, so it works with different currencies.

func setupTextField() {
        textField.delegate = self
        textField.tintColor = .clear
        textField.keyboardType = .numberPad
}


func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    setFormattedAmount(string)
    
    return false
}

private func setFormattedAmount(_ string: String) {
    switch string {
    case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9":
        amountString = amountString + string
    default:
        if amountString.count > 0 {
            amountString.removeLast()
        }
    }
    
    let amount = (NSString(string: amountString).doubleValue) / 100
    textField.text = formatAmount(amount)
}

private func formatAmount(_ amount: Double) -> String {
    let formatter = NumberFormatter()
    formatter.numberStyle = .currency
    formatter.locale = .current
    
    if let amount = formatter.string(from: NSNumber(value: amount)) {
        return amount
    }
    
    return ""
}
Republicanize answered 17/10, 2020 at 9:36 Comment(1)
I used the code with some modifications.Ovida
V
0

Here is a code for swift 2

@IBOutlet weak var txtAmount: UITextField!

//MARK: - UITextField Delegate -
    func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool{

        if string.characters.count == 0 {
            return true
        }

        let userEnteredString = textField.text ?? ""
        var newString = (userEnteredString as NSString).stringByReplacingCharactersInRange(range, withString: string) as NSString
        newString = newString.stringByReplacingOccurrencesOfString(".", withString: "")

        let centAmount : NSInteger = newString.integerValue
        let amount = (Double(centAmount) / 100.0)

        if newString.length < 16 {
            let str = String(format: "%0.2f", arguments: [amount])
            txtAmount.text = str
        }

        return false //return false for exact out put
    }

Note : Connect delegate for textField from storyboard or programatically

Vetiver answered 18/5, 2018 at 4:16 Comment(0)
O
0

Just for fun: copied Thomas's answer (full credits -and points- to him please) into a file to run as a Swift 4.1 script (with minor fixes):

dotnum.swift:

#!/usr/bin/swift

struct DotNum {
    private var fraction:String = ""
    private var intval:String = ""
    init() {}
    mutating func enter(_ s:String) {
        if fraction.count < 2 {
          fraction = s + fraction
        } else {
          intval = s + intval
        }
    }
    private var sFract:String {
        if fraction.count == 0 { return "00" }
        if fraction.count == 1 { return "0\(fraction)" }
        return fraction
    }
    var stringVal:String {
        if intval == ""  { return "0.\(sFract)" }
        return "\(intval).\(sFract)"
    }
}

var val = DotNum()
val.enter("1")
print(val.stringVal)
val.enter("2")
print(val.stringVal)
val.enter("3")
print(val.stringVal)
val.enter("4")
print(val.stringVal)

Then run it in a terminal:

$ chmod +x dotnum.swift
$ ./dotnum.swift
0.01
0.21
3.21
43.21
Ocher answered 18/5, 2018 at 5:21 Comment(0)
M
0

Thanks to everyone here. From all the answers here I managed to come out with mine.

First I set up the initial value of the textField to be:

private func commonInit() { 
    amountTextField.text = "0.00"
}

Then I use the UITextFieldDelegate to get the input value and the current textview.text:

func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    //Need to check if the textfield.text can be evaluated as number or not before passing it to the function
    //Get the current text value, and current user input and pass it to the 
    let formattedAmount = formatAmount(oldAmount: textField.text, userInput: string)
    textField.text = formattedAmount
    return false
}

Here go my private function to format the number to move from right to left:

private func formatAmount(currentText: String, userInput: String) -> String {
    let amount = currentText.components(separatedBy: ".")
    var intValue: String = amount[0]
    var decimalValue: String = amount[1]
    

    //backspace registered, need to move the number to the right
    if userInput.isEmpty {
        decimalValue.remove(at: decimalValue.index(before: decimalValue.endIndex))
        decimalValue = intValue.last!.string + decimalValue
        intValue.remove(at: intValue.index(before: intValue.endIndex))
        if intValue.isEmpty {
            intValue = "0"
        }
    } else {
        
        //Need to consider if user paste value
        if userInput.count > 2 {
            decimalValue = String(userInput.suffix(2))
            intValue = String(userInput.dropLast(2))
        } else {
            decimalValue = rmAmount[1] + userInput
            
            //Add to int value (move to the right)
            intValue = intValue + decimalValue.first!.string
            
            if Int(intValue) == 0 {
                intValue = "0"      //00 -> 0
            } else if intValue.first == "0" {
                //remove 0 from at the first position in intValue
                intValue.remove(at: intValue.startIndex)    //01 -> 1
            }
            
            //Remove tenth place from decimal value since it goes to Int already
            decimalValue.remove(at: decimalValue.startIndex)
        }
    }
    return intValue + "." + decimalValue
}

This is basically it. Other extra implementations can be added by your own initiatives. Let me know if there is any problem with my implementation.

PS: This is of course only works for certain currency only, in my case, my apps is set up only for that local so thats why I use this way.

Mcclain answered 4/9, 2020 at 3:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.