Formatting a Uitextfield for Credit Card Input Like (Xxxx Xxxx Xxxx Xxxx)

Formatting a UITextField for credit card input like (xxxx xxxx xxxx xxxx)

If you're using Swift, go read my port of this answer for Swift 4 and use that instead.

If you're in Objective-C...

Firstly, to your UITextFieldDelegate, add these instance variables...

NSString *previousTextFieldContent;
UITextRange *previousSelection;

... and these methods:

// Version 1.3
// Source and explanation: http://stackoverflow.com/a/19161529/1709587
-(void)reformatAsCardNumber:(UITextField *)textField
{
// In order to make the cursor end up positioned correctly, we need to
// explicitly reposition it after we inject spaces into the text.
// targetCursorPosition keeps track of where the cursor needs to end up as
// we modify the string, and at the end we set the cursor position to it.
NSUInteger targetCursorPosition =
[textField offsetFromPosition:textField.beginningOfDocument
toPosition:textField.selectedTextRange.start];

NSString *cardNumberWithoutSpaces =
[self removeNonDigits:textField.text
andPreserveCursorPosition:&targetCursorPosition];

if ([cardNumberWithoutSpaces length] > 19) {
// If the user is trying to enter more than 19 digits, we prevent
// their change, leaving the text field in its previous state.
// While 16 digits is usual, credit card numbers have a hard
// maximum of 19 digits defined by ISO standard 7812-1 in section
// 3.8 and elsewhere. Applying this hard maximum here rather than
// a maximum of 16 ensures that users with unusual card numbers
// will still be able to enter their card number even if the
// resultant formatting is odd.
[textField setText:previousTextFieldContent];
textField.selectedTextRange = previousSelection;
return;
}

NSString *cardNumberWithSpaces =
[self insertCreditCardSpaces:cardNumberWithoutSpaces
andPreserveCursorPosition:&targetCursorPosition];

textField.text = cardNumberWithSpaces;
UITextPosition *targetPosition =
[textField positionFromPosition:[textField beginningOfDocument]
offset:targetCursorPosition];

[textField setSelectedTextRange:
[textField textRangeFromPosition:targetPosition
toPosition:targetPosition]
];
}

-(BOOL)textField:(UITextField *)textField
shouldChangeCharactersInRange:(NSRange)range
replacementString:(NSString *)string
{
// Note textField's current state before performing the change, in case
// reformatTextField wants to revert it
previousTextFieldContent = textField.text;
previousSelection = textField.selectedTextRange;

return YES;
}

/*
Removes non-digits from the string, decrementing `cursorPosition` as
appropriate so that, for instance, if we pass in `@"1111 1123 1111"`
and a cursor position of `8`, the cursor position will be changed to
`7` (keeping it between the '2' and the '3' after the spaces are removed).
*/
- (NSString *)removeNonDigits:(NSString *)string
andPreserveCursorPosition:(NSUInteger *)cursorPosition
{
NSUInteger originalCursorPosition = *cursorPosition;
NSMutableString *digitsOnlyString = [NSMutableString new];
for (NSUInteger i=0; i<[string length]; i++) {
unichar characterToAdd = [string characterAtIndex:i];
if (isdigit(characterToAdd)) {
NSString *stringToAdd =
[NSString stringWithCharacters:&characterToAdd
length:1];

[digitsOnlyString appendString:stringToAdd];
}
else {
if (i < originalCursorPosition) {
(*cursorPosition)--;
}
}
}

return digitsOnlyString;
}

/*
Detects the card number format from the prefix, then inserts spaces into
the string to format it as a credit card number, incrementing `cursorPosition`
as appropriate so that, for instance, if we pass in `@"111111231111"` and a
cursor position of `7`, the cursor position will be changed to `8` (keeping
it between the '2' and the '3' after the spaces are added).
*/
- (NSString *)insertCreditCardSpaces:(NSString *)string
andPreserveCursorPosition:(NSUInteger *)cursorPosition
{
// Mapping of card prefix to pattern is taken from
// https://baymard.com/checkout-usability/credit-card-patterns

// UATP cards have 4-5-6 (XXXX-XXXXX-XXXXXX) format
bool is456 = [string hasPrefix: @"1"];

// These prefixes reliably indicate either a 4-6-5 or 4-6-4 card. We treat all
// these as 4-6-5-4 to err on the side of always letting the user type more
// digits.
bool is465 = [string hasPrefix: @"34"] ||
[string hasPrefix: @"37"] ||

// Diners Club
[string hasPrefix: @"300"] ||
[string hasPrefix: @"301"] ||
[string hasPrefix: @"302"] ||
[string hasPrefix: @"303"] ||
[string hasPrefix: @"304"] ||
[string hasPrefix: @"305"] ||
[string hasPrefix: @"309"] ||
[string hasPrefix: @"36"] ||
[string hasPrefix: @"38"] ||
[string hasPrefix: @"39"];

// In all other cases, assume 4-4-4-4-3.
// This won't always be correct; for instance, Maestro has 4-4-5 cards
// according to https://baymard.com/checkout-usability/credit-card-patterns,
// but I don't know what prefixes identify particular formats.
bool is4444 = !(is456 || is465);

NSMutableString *stringWithAddedSpaces = [NSMutableString new];
NSUInteger cursorPositionInSpacelessString = *cursorPosition;
for (NSUInteger i=0; i<[string length]; i++) {
bool needs465Spacing = (is465 && (i == 4 || i == 10 || i == 15));
bool needs456Spacing = (is456 && (i == 4 || i == 9 || i == 15));
bool needs4444Spacing = (is4444 && i > 0 && (i % 4) == 0);

if (needs465Spacing || needs456Spacing || needs4444Spacing) {
[stringWithAddedSpaces appendString:@" "];
if (i < cursorPositionInSpacelessString) {
(*cursorPosition)++;
}
}
unichar characterToAdd = [string characterAtIndex:i];
NSString *stringToAdd =
[NSString stringWithCharacters:&characterToAdd length:1];

[stringWithAddedSpaces appendString:stringToAdd];
}

return stringWithAddedSpaces;
}

Secondly, set reformatCardNumber: to be called whenever the text field fires a UIControlEventEditingChanged event:

[yourTextField addTarget:yourTextFieldDelegate 
action:@selector(reformatAsCardNumber:)
forControlEvents:UIControlEventEditingChanged];

(Of course, you'll need to do this at some point after your text field and its delegate have been instantiated. If you're using storyboards, the viewDidLoad method of your view controller is an appropriate place.

Some Explanation

This is a deceptively complicated problem. Three important issues that may not be immediately obvious (and which previous answers here all fail to take into account):

  1. While the XXXX XXXX XXXX XXXX format for credit and debit card numbers is the most common one, it's not the only one. For example, American Express cards have 15 digit numbers usually written in XXXX XXXXXX XXXXX format, like this:

    An American Express card

    Even Visa cards can have fewer than 16 digits, and Maestro cards can have more:

    A Russian Maestro card with 18 digits

  2. There are more ways for the user to interact with a text field than just typing in single characters at the end of their existing input. You also have to properly handle the user adding characters in the middle of the string, deleting single characters, deleting multiple selected characters, and pasting in multiple characters. Some simpler/more naive approaches to this problem will fail to handle some of these interactions properly. The most perverse case is a user pasting in multiple characters in the middle of the string to replace other characters, and this solution is general enough to handle that.

  3. You don't just need to reformat the text of the text field properly after the user modifies it - you also need to position the text cursor sensibly. Naive approaches to the problem that don't take this into account will almost certainly end up doing something silly with the text cursor in some cases (like putting it to the end of the text field after the user adds a digit in the middle of it).

To deal with issue #1, we use the partial mapping of card number prefixes to formats curated by The Baymard Institute at https://baymard.com/checkout-usability/credit-card-patterns. We can automatically detect the the card provider from the first couple of digits and (in some cases) infer the format and adjust our formatting accordingly. Thanks to cnotethegr8 for contributing this idea to this answer.

The simplest and easiest way to deal with issue #2 (and the way used in the code above) is to strip out all spaces and reinsert them in the correct positions every time the content of the text field changes, sparing us the need to figure out what kind of text manipulation (an insertion, a deletion, or a replacement) is going on and handle the possibilities differently.

To deal with issue #3, we keep track of how the desired index of the cursor changes as we strip out non-digits and then insert spaces. This is why the code rather verbosely performs these manipulations character-by-character using NSMutableString, rather than using NSString's string replacement methods.

Finally, there's one more trap lurking: returning NO from textField: shouldChangeCharactersInRange: replacementString breaks the 'Cut' button the user gets when they select text in the text field, which is why I don't do it. Returning NO from that method results in 'Cut' simply not updating the clipboard at all, and I know of no fix or workaround. As a result, we need to do the reformatting of the text field in a UIControlEventEditingChanged handler instead of (more obviously) in shouldChangeCharactersInRange: itself.

Luckily, the UIControl event handlers seem to get called before UI updates get flushed to the screen, so this approach works fine.

There are also a whole bunch of minor questions about exactly how the text field should behave that don't have obvious correct answers:

  • If the user tries to paste in something that would cause the content of the text field to exceed 19 digits, should the beginning of the pasted string be inserted (until 19 digits are reached) and the remainder cropped, or should nothing be inserted at all?
  • If the user tries to delete a single space by positioning their cursor after it and pressing the backspace key, should nothing happen and the cursor remain where it is, should the cursor move left one character (placing it before the space), or should the digit to the left of the space be deleted as though the cursor were already left of the space?
  • When the user types in the fourth, eighth, or twelfth digit, should a space be immediately inserted and the cursor moved after it, or should the space only be inserted after the user types the fifth, ninth, or thirteenth digit?
  • When the user deletes the first digit after a space, if this doesn't cause the space to be removed entirely, should this lead to their cursor being positioned before or after the space?

Probably any answer to any of these questions will be adequate, but I list them just to make clear that there are actually a lot of special cases that you might want to think carefully about here, if you were obsessive enough. In the code above, I've picked answers to these questions that seemed reasonable to me. If you happen to have strong feelings about any of these points that aren't compatible with the way my code behaves, it should be easy enough to tweak it to your needs.

How Create TextField Like Credit Card Number in flutter?

Try below code hope its helpful to you.

Your Widget:

TextFormField(
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
CardNumberFormatter(),
],
textInputAction: TextInputAction.done,
keyboardType: TextInputType.number,
decoration: InputDecoration(
prefixIcon: Padding(
padding: const EdgeInsets.all(8.0),
child: Image.network(
'https://upload.wikimedia.org/wikipedia/commons/thumb/2/2a/Mastercard-logo.svg/800px-Mastercard-logo.svg.png',
height: 30,
width: 30,
),
),
suffixIcon: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Change',
style: TextStyle(color: Colors.green),
),
),
border: OutlineInputBorder(),
hintText: 'XXXX XXXX XXXX XXXX',
labelText: 'Card Number',
),
maxLength: 19,
onChanged: (value) {},
),

Create class for seprate the digits

class CardNumberFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue previousValue,
TextEditingValue nextValue,
) {
var inputText = nextValue.text;

if (nextValue.selection.baseOffset == 0) {
return nextValue;
}

var bufferString = new StringBuffer();
for (int i = 0; i < inputText.length; i++) {
bufferString.write(inputText[i]);
var nonZeroIndexValue = i + 1;
if (nonZeroIndexValue % 4 == 0 && nonZeroIndexValue != inputText.length) {
bufferString.write(' ');
}
}

var string = bufferString.toString();
return nextValue.copyWith(
text: string,
selection: new TextSelection.collapsed(
offset: string.length,
),
);
}
}

Full Example:

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: Center(
child: MyWidget(),
),
),
);
}
}

class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.symmetric(
horizontal: 20,
),
child: TextFormField(
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
CardNumberFormatter(),
],
textInputAction: TextInputAction.done,
keyboardType: TextInputType.number,
decoration: InputDecoration(
prefixIcon: Padding(
padding: const EdgeInsets.all(8.0),
child: Image.network(
'https://upload.wikimedia.org/wikipedia/commons/thumb/2/2a/Mastercard-logo.svg/800px-Mastercard-logo.svg.png',
height: 30,
width: 30,
),
),
suffixIcon: Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Change',
style: TextStyle(color: Colors.green),
),
),
border: OutlineInputBorder(),
hintText: 'XXXX XXXX XXXX XXXX',
labelText: 'Card Number',
),
maxLength: 19,
onChanged: (value) {},
),
);
}
}

class CardNumberFormatter extends TextInputFormatter {
@override
TextEditingValue formatEditUpdate(
TextEditingValue previousValue,
TextEditingValue nextValue,
) {
var inputText = nextValue.text;

if (nextValue.selection.baseOffset == 0) {
return nextValue;
}

var bufferString = new StringBuffer();
for (int i = 0; i < inputText.length; i++) {
bufferString.write(inputText[i]);
var nonZeroIndexValue = i + 1;
if (nonZeroIndexValue % 4 == 0 && nonZeroIndexValue != inputText.length) {
bufferString.write(' ');
}
}

var string = bufferString.toString();
return nextValue.copyWith(
text: string,
selection: new TextSelection.collapsed(
offset: string.length,
),
);
}
}

Test Your code on Darpad

Your Result Screen-> image

How to apply format while typing UITextField

I don't think it is worthy to save the caret position when entering a credit card number (or login card). Just implement the deleteBackward method and remove the last digit. The user can always retype the numbers from left to right. I would also subclass textfield and create a custom field. So first remove all non digits from the field and then switch the number of digits left. If there is from 3 to 11 just insert the first hyphen. If there is 12 insert the first and second hyphens. If there is 13 add the third hyphen as well. Make sure to insert them in reverse other to make your life easier:

class LoginField: UITextField {
override func didMoveToSuperview() {
keyboardType = .numberPad
textAlignment = .center
autocorrectionType = .no
addTarget(self, action: #selector(editingChanged), for: .editingChanged)
}
override func deleteBackward() {
text?.removeAll(where: \.isDigit.negated)
let _ = text?.popLast()
sendActions(for: .editingChanged)
}
}


extension LoginField {
@objc func editingChanged(_ textField: UITextField) {
text?.removeAll(where: \.isDigit.negated)
text = text?.prefix(15).string
if let offset = text?.count,
let startIndex = text?.startIndex {
switch offset {
case 3...11:
if let index = text?.index(startIndex, offsetBy: 3) {
text?.insert("-", at: index)
}
case 12:
if let index = text?.index(startIndex, offsetBy: 12) {
text?.insert("-", at: index)
}

if let index = text?.index(startIndex, offsetBy: 3) {
text?.insert("-", at: index)
}
case 13...15:
if let index = text?.index(startIndex, offsetBy: 13) {
text?.insert("-", at: index)
}
if let index = text?.index(startIndex, offsetBy: 12) {
text?.insert("-", at: index)
}
if let index = text?.index(startIndex, offsetBy: 3) {
text?.insert("-", at: index)
}
default: break
}
}
}
}


extension Bool {
var negated: Bool { !self }
}


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


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

How do I format a string to Credit card format

Try this:

-(NSString *) correctString:(NSString *) anyStr {

NSMutableString *str=[NSMutableString stringWithString:anyStr];
int indx=4;
while (indx<str.length) {
[str insertString:@" " atIndex:indx];
indx +=5;
}
anyStr=str;
return anyStr;
}


Related Topics



Leave a reply



Submit