Search code examples
iosswiftregexuitextfieldcurrency-formatting

Formatting Currency - Swift


I am looking for a textfield currency formatter such that it fulfils the following criterias:

  1. It should be formatted(comma separated) as I am typing
  2. 10 digits before decimal point and 2 digits after it, should be allowed
  3. It should allow a regex for (2)
  4. When we cut, cursor should remain at the same place
  5. When we type in the middle of the currency, cursor should not shift to left.
  6. It should support localization (Commas and Periods) in regex.

I have tried alot of solutions:

  1. Using NSCharacterSet (This is the closest but regex fails here due to interchange of . and , during localization, also we have used .decimal type here to avoid the $ in textField)

    class func checkTextField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
        guard let textBeforeEditing = textField.text else {
            return true
        }
        if ((string == "0" || string == "") && (textField.text! as NSString).range(of: ".").location < range.location) {
            return true
        }
        var currentPosition = 0
        if let selectedRange = textField.selectedTextRange {
            currentPosition = textField.offset(from: textField.beginningOfDocument, to: string == "" ? selectedRange.end : selectedRange.start)
        }
        let allowedCharacterSet = NSCharacterSet(charactersIn: "0123456789.").inverted
        let filtered = string.components(separatedBy: allowedCharacterSet)
        let component = filtered.joined(separator: "")
        let isNumeric = string.replacingOccurrences(of: ",", with: "") == component
        var textFieldString : String = ""
        var numberWithoutCommas : String = ""
        guard isNumeric else {
            return false
        }
        let formatter = NumberFormatter()
        formatter.numberStyle = .decimal
        textFieldString = (textField.text! as NSString).replacingCharacters(in: range, with: string)
        numberWithoutCommas = textFieldString.replacingOccurrences(of: ",", with: "")
        let formattedNumberWithoutCommas = formatter.number(from: numberWithoutCommas)
        guard let formattedNumber = formattedNumberWithoutCommas, var formattedString = formatter.string(from: formattedNumber) else {
            textField.text = nil
            return false
        }
        if string == "." && range.location == textField.text?.count {
            formattedString = formattedString.appending(".")
        }
        textField.text = formattedString
        currentPosition = getCursorPositionForTextField(string: string, cursorPosition: currentPosition, formattedString: formattedString, textBeforeEditing: textBeforeEditing)
        handleTextFieldCursor(cursorPosition: currentPosition, textField: textField)
        return false
    }
    
  2. Using NumberFormatter but cursor shifts to end on every cut/paste

    extension String {
    
        func currencyInputFormatting() -> String {
    
            var number: NSNumber!
            let formatter = NumberFormatter()
            formatter.numberStyle = .currency
            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.characters.count), withTemplate: "")
    
            let double = (amountWithPrefix as NSString).doubleValue
            number = NSNumber(value: (double / 100))
    
            guard number != 0 as NSNumber else {
                return ""
            }
    
            return formatter.string(from: number)!
        }
    }
    

I have spent almost a day or two finding a 100% workable solution but not able to resolve. Any help will be appreciated

EDIT

I have come quite close to the solution with the help of the @denis_lor answer but still unable to achieve the interchange of comma with period. Here's my updated code, am I missing something? It works fine with english but not with spanish.

class func checkTextField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
    guard let textBeforeEditing = textField.text else {
        return true
    }
    if ((string == "0" || string == "") && (textField.text! as NSString).range(of: "\(NSLocalizedString("core_decimal_separator_symbol", comment: ""))").location < range.location) {
        return true
    }
    var currentPosition = 0
    if let selectedRange = textField.selectedTextRange {
        currentPosition = textField.offset(from: textField.beginningOfDocument, to: string == "" ? selectedRange.end : selectedRange.start)
    }
    let allowedCharacterSet = NSCharacterSet(charactersIn: "0123456789\(NSLocalizedString("core_decimal_separator_symbol", comment: ""))").inverted
    let filtered = string.components(separatedBy: allowedCharacterSet)
    let component = filtered.joined(separator: "")
    let isNumeric = string.replacingOccurrences(of: NSLocalizedString("core_thousand_separator_symbol", comment: ""), with: "") == component
    var textFieldString : String = ""
    var numberWithoutCommas : String = ""
    guard isNumeric else {
        return false
    }
    let formatter = NumberFormatter()
    formatter.numberStyle = .decimal
    textFieldString = (textField.text! as NSString).replacingCharacters(in: range, with: string)
    numberWithoutCommas = textFieldString.replacingOccurrences(of: NSLocalizedString("core_thousand_separator_symbol", comment: ""), with: "")
    let formattedNumberWithoutCommas = formatter.number(from: numberWithoutCommas)
    guard let formattedNumber = formattedNumberWithoutCommas, var formattedString = formatter.string(from: formattedNumber) else {
        textField.text = nil
        return false
    }
    if string == NSLocalizedString("core_decimal_separator_symbol", comment: "") && range.location == textField.text?.count {
        formattedString = formattedString.appending(NSLocalizedString("core_decimal_separator_symbol", comment: ""))
    }
    textField.text = formattedString
    currentPosition = getCursorPositionForTextField(string: string, cursorPosition: currentPosition, formattedString: formattedString, textBeforeEditing: textBeforeEditing)
    handleTextFieldCursor(cursorPosition: currentPosition, textField: textField)
    return false
}

Solution

  • Ok so it looks your concern here could be solved by making a first round implementation of your first solution, where you only need to think about localization of , and .. That is easy, you could implement it in many different ways, but the important part is you have your app for example localized in let's say two language that treats decimals and thousands with different symbols (let's assume as an example those languages are english and italian):

    • [en] language treats the separation of decimals with a , and thousands with a .
    • [it] language treats the separation of decimals with a . and thousands with a ,

    A) What you could do is to create a Localizable.strings file and then localize your project in let's say english and italian as an example. To do it add the language here. enter image description here

    B) Then go to your Localizable.strings file and localize it for the languages you support (English and Italian as an example), like in this image that was done for German and English enter image description here

    You will end up with two Localizable.strings now, one for English and one for Italian:

    Localizable.strings (English)

    core_decimal_separator_symbol = ",";
    core_thousand_separator_symbol = ".";
    

    Localizable.strings (Italian)

    core_decimal_separator_symbol = ".";
    core_thousand_separator_symbol = ",";
    

    C) And in your code, everywhere you need to address, for example, your decimal separator symbol, instead of writing it hard coded you could do something like:

    removeDecimalSeparator = numberAsString.replacingOccurrences(of: NSLocalizedString("core_decimal_separator_symbol", comment: ""), with: "")
    

    So whenever your app is localized to English for example this code will traslate into:

    removeDecimalSeparator = numberAsString.replacingOccurrences(of: ",", with: "")
    

    And when your app is localized to Italian for example this code will traslate into:

    removeDecimalSeparator = numberAsString.replacingOccurrences(of: ".", with: "")
    

    To conclude: consider these as example taking into account the Localizable.strings we have in this answer. Just to show you how you could manipulate some symbols in different ways for different languages by using Localization in your app.