Cursor Shifts to End on Edit of Formatted Decimal Textfield - Swift

Cursor goes to end after setting text to UITextField inside shouldChangeCharactersIn

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

guard CharacterSet(charactersIn: "0123456789").isSuperset(of: CharacterSet(charactersIn: string)) else {
return false
}
if let text = textField.text {
let newLength = text.count + string.count - range.length
let newText = text + string

let textFieldText: NSString = (textField.text ?? "") as NSString
let txtAfterUpdate = textFieldText.replacingCharacters(in: range, with: string)
if(newLength <= 15){

var currentPosition = 0
if let selectedRange = textField.selectedTextRange {
currentPosition = textField.offset(from: textField.beginningOfDocument, to: selectedRange.start)
}

if(string == ""){
if(currentPosition == 2){
if(textField.text!.count > 2){
currentPosition = 1
}else{
currentPosition = 0
}
}else if(currentPosition == 7){
currentPosition = 4
}else if(currentPosition == 11){
currentPosition = 9
}else if(currentPosition == 14){
currentPosition = 12
}else{
if(currentPosition != 1){
currentPosition = currentPosition - 1
}
}
}else{
if(currentPosition == 0){
currentPosition = 2
}else if(currentPosition == 4){
currentPosition = 7
}else if(currentPosition == 9){
currentPosition = 11
}else if(currentPosition == 12){
currentPosition = 14
}else{
currentPosition = currentPosition + 1
}
}

textField.text = txtAfterUpdate.applyPatternOnNumbers()
print("textField Length -> : \(textField.text?.count ?? 0)")

if let newPosition = textField.position(from: textField.beginningOfDocument, offset: currentPosition) {
textField.selectedTextRange = textField.textRange(from: newPosition, to: newPosition)
}

return false
}
return newLength <= 15
}
return true
}

Trying to format a UITextField to behave like a currency calculator for only numeric input in iOS

Here's a nice way to do it. Remember to set your UITextField to self in the viewDidLoad method and your header file must conform to the UITextFieldDelegate protocol

-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string{

NSString *cleanCentString = [[textField.text componentsSeparatedByCharactersInSet: [[NSCharacterSet decimalDigitCharacterSet] invertedSet]] componentsJoinedByString:@""];
NSInteger centValue = [cleanCentString intValue];
NSNumberFormatter * f = [[NSNumberFormatter alloc] init];
NSNumber *myNumber = [f numberFromString:cleanCentString];
NSNumber *result;

if([textField.text length] < 16){
if (string.length > 0)
{
centValue = centValue * 10 + [string intValue];
double intermediate = [myNumber doubleValue] * 10 + [[f numberFromString:string] doubleValue];
result = [[NSNumber alloc] initWithDouble:intermediate];
}
else
{
centValue = centValue / 10;
double intermediate = [myNumber doubleValue]/10;
result = [[NSNumber alloc] initWithDouble:intermediate];
}

myNumber = result;
NSLog(@"%ld ++++ %@", (long)centValue, myNumber);
NSNumber *formatedValue;
formatedValue = [[NSNumber alloc] initWithDouble:[myNumber doubleValue]/ 100.0f];
NSNumberFormatter *_currencyFormatter = [[NSNumberFormatter alloc] init];
[_currencyFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
textField.text = [_currencyFormatter stringFromNumber:formatedValue];
return NO;
}else{

NSNumberFormatter *_currencyFormatter = [[NSNumberFormatter alloc] init];
[_currencyFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
textField.text = [_currencyFormatter stringFromNumber:00];

UIAlertView *alert = [[UIAlertView alloc]initWithTitle: @"Deposit Amount Limit"
message: @"You've exceeded the deposit amount limit. Kindly re-input amount"
delegate: self
cancelButtonTitle:@"Cancel"
otherButtonTitles:@"OK",nil];

[alert show];
return NO;
}
return YES;
}

Formatting Currency - Swift

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.
Sample Image

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
Sample Image

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.

UITextField display formatted as currency without decimal

Try below code

extension ViewController: UITextFieldDelegate {

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

let text: NSString = (textField.text ?? "") as NSString
let finalString = text.replacingCharacters(in: range, with: string)

// 'currency' is a String extension that doews all the number styling
amountTextField.text = finalString.currency

// returning 'false' so that textfield will not be updated here, instead from styling extension
return false
}
}

extension String {
var currency: String {
// removing all characters from string before formatting
let stringWithoutSymbol = self.replacingOccurrences(of: "$", with: "")
let stringWithoutComma = stringWithoutSymbol.replacingOccurrences(of: ",", with: "")

let styler = NumberFormatter()
styler.minimumFractionDigits = 0
styler.maximumFractionDigits = 0
styler.currencySymbol = "$"
styler.numberStyle = .currency

if let result = NumberFormatter().number(from: stringWithoutComma) {
return styler.string(from: result)!
}

return self
}
}

Real-time NSTextField formatter in SwiftUI context

After some additional trial and error, I was able to fix the cursor problems mentioned in my initial question. The version here is, to the best of my knowledge, bullet proof (though the test team will have a whack at it so perhaps it will change).

Would still welcome any improvement suggestions.

import Foundation
import SwiftUI

struct NumberField : NSViewRepresentable {
typealias NSViewType = NumberText
var defaultText : String
var maxDigits : Int
var numberValue : Binding<Int>

func makeNSView(context: Context) -> NSViewType {

// Create text field
let numberTextField = NumberText()
numberTextField.isEditable = true
numberTextField.configure(text: defaultText, digits: maxDigits, intBinding: numberValue)

return numberTextField
}

func updateNSView(_ nsView: NSViewType, context: Context) {

}

}

/// NumberText draws an NSTextField that will accept only digits up to a maximum number specified when calling Configure. Apple implements some nice integration between SwiftUI's frame and padding modifiers and the NSTextField's designated initializer. Rather than having to figure out how to fix/preserve this integration, this class provides a "configure()" function that is effectively it's initializer. As a result, it is MANDATORY that this class's configure() function be called immediately after initializing the class.
class NumberText : NSTextField {

// Code below jumps through a couple of hoops to avoid having to write a custom initializer since that gets in the middle of Apple's configuration of the text field using standard SwiftUI modifiers.
// NOTE THAT A USER OF NumberText MUST CALL CONFIGURE() IMMEDIATELY AFTER CREATING IT

// The following variable declarations are all immediately initialized to avoid having to write an init() function
var numberBinding : Binding<Int> = Binding( // This is initialized with a garbage Binding just to avoid having to write an initializer
get: {return -1},
set: {newValue in return}
)
var defaultText = "Default String"
var maxDigits = 9
private var decimalFormatter = NumberFormatter()


func configure(text: String, digits: Int, intBinding: Binding<Int>) { // Configure is used here instead of attempting to override init()
// Configure values
decimalFormatter.numberStyle = .decimal
defaultText = text
self.placeholderString = defaultText
maxDigits = digits
numberBinding = intBinding

// Make sure that default text is shown if numberBinding.wrappedValue is 0
if numberBinding.wrappedValue == 0 {self.stringValue = ""}
}

override func textDidChange(_ notification: Notification) {
self.stringValue = numberTextFromString(self.stringValue, maxLength: maxDigits) // numberTextFromString() also sets the wrappedValue of numberBinding
if self.stringValue == "0" {self.stringValue = ""}
}

/// Takes in string from text field and returns the best number string that can be made from it by removing any non-numeric characters and adding comma separators in the right places.
/// Along the way, self.numberBinding.warppedValue is set to the Int corresponding to the output string and self's cursor is reset to account for the erasure of invalid characters and the addition of commas
/// - Parameters:
/// - inputText: Incoming text from text field
/// - maxLength: Maximum number of digits allowed in this field
/// - Returns:String representing number
func numberTextFromString(_ inputText: String, maxLength: Int) -> String {

var decrementCursorForInvalidChar = 0
var incomingDigitsBeforeCursor = 0

// For cursor calculation, find digit count behind cursor in incoming string
// Get incoming cursor location
let incomingCursorLocation = currentEditor()?.selectedRange.location ?? 0
// Create prefix behind incoming cursor location
let incomingPrefixToCursor = inputText.prefix(incomingCursorLocation)
// Count digits in prefix
for character in incomingPrefixToCursor {
if character.isNumber == true {
incomingDigitsBeforeCursor += 1
}
}

// Create a filtered and trucated version of inputText
var characterCount = 0
let filteredText = inputText.filter { character in
characterCount += 1
if character.isNumber == true {
return true
} else { // character is invalid or comma.
if character != "," { // character is invalid,
if characterCount < inputText.count { // Only decrement cursor if not at end of string
// Decrement cursor
decrementCursorForInvalidChar += 1
}
}
return false
}
}
// Decrement cursor as needed for invalid character entries
currentEditor()!.selectedRange.location = incomingCursorLocation - decrementCursorForInvalidChar

let truncatedText = String(filteredText.prefix(maxLength))

// Make a number from truncated text
let myNumber = Int(truncating: decimalFormatter.number(from: truncatedText) ?? 0 )
// Set binding value
numberBinding.wrappedValue = myNumber

// Create formatted string for return
let outgoingString = decimalFormatter.string(from: myNumber as NSNumber) ?? "?"

// For cursor calculation, find character representing incomingDigitsBeforeCursor.lastIndex
var charCount = 0
var digitCount = 0
var charIndex = outgoingString.startIndex
while digitCount < incomingDigitsBeforeCursor && charCount < outgoingString.count {
charIndex = outgoingString.index(outgoingString.startIndex, offsetBy: charCount)
charCount += 1
if outgoingString[charIndex].isNumber == true {
digitCount += 1
}
}
// Get integer corresponding to current charIndex
let outgoingCursorLocation = outgoingString.distance(from: outgoingString.startIndex, to: charIndex) + 1
currentEditor()!.selectedRange.location = outgoingCursorLocation

return outgoingString
}

}


Related Topics



Leave a reply



Submit