I want to format a UITextField
for entering a credit card number into such that it only allows digits to be entered and automatically inserts spaces so that the number is formatted like so:
XXXX XXXX XXXX XXXX
How can I do this?
I want to format a UITextField
for entering a credit card number into such that it only allows digits to be entered and automatically inserts spaces so that the number is formatted like so:
XXXX XXXX XXXX XXXX
How can I do this?
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: https://mcmap.net/q/258955/-formatting-a-uitextfield-for-credit-card-input-like-xxxx-xxxx-xxxx-xxxx
-(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.
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):
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:
Even Visa cards can have fewer than 16 digits, and Maestro cards can have more:
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.
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:
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.
unrecognized selector sent to instance
and a thread problem when I do this. Ideas? –
Inaccurate targetCursorPosition
at the beginning is broken. Try adding a Character that is not a unicode Scalar, like an emoji. Cursor position will be inaccurate. –
Steepen Below is a working Swift 4 port of Logicopolis's answer (which is in turn a Swift 2 port of an old version of my accepted answer in Objective-C) enhanced with cnotethegr8 's trick for supporting Amex cards and then further enhanced to support more card formats. I suggest looking over the accepted answer if you haven't already, since it helps explain the motivation behind a lot of this code.
Note that the minimal series of steps needed to see this in action is:
Main.storyboard
, add a Text Field.ViewController
the delegate of the Text Field.ViewController.swift
.IBOutlet
to the Text Field.import UIKit
class ViewController: UIViewController, UITextFieldDelegate {
private var previousTextFieldContent: String?
private var previousSelection: UITextRange?
@IBOutlet var yourTextField: UITextField!;
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view, typically from a nib
yourTextField.addTarget(self, action: #selector(reformatAsCardNumber), for: .editingChanged)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
previousTextFieldContent = textField.text;
previousSelection = textField.selectedTextRange;
return true
}
@objc func reformatAsCardNumber(textField: UITextField) {
var targetCursorPosition = 0
if let startPosition = textField.selectedTextRange?.start {
targetCursorPosition = textField.offset(from: textField.beginningOfDocument, to: startPosition)
}
var cardNumberWithoutSpaces = ""
if let text = textField.text {
cardNumberWithoutSpaces = self.removeNonDigits(string: text, andPreserveCursorPosition: &targetCursorPosition)
}
if cardNumberWithoutSpaces.count > 19 {
textField.text = previousTextFieldContent
textField.selectedTextRange = previousSelection
return
}
let cardNumberWithSpaces = self.insertCreditCardSpaces(cardNumberWithoutSpaces, preserveCursorPosition: &targetCursorPosition)
textField.text = cardNumberWithSpaces
if let targetPosition = textField.position(from: textField.beginningOfDocument, offset: targetCursorPosition) {
textField.selectedTextRange = textField.textRange(from: targetPosition, to: targetPosition)
}
}
func removeNonDigits(string: String, andPreserveCursorPosition cursorPosition: inout Int) -> String {
var digitsOnlyString = ""
let originalCursorPosition = cursorPosition
for i in Swift.stride(from: 0, to: string.count, by: 1) {
let characterToAdd = string[string.index(string.startIndex, offsetBy: i)]
if characterToAdd >= "0" && characterToAdd <= "9" {
digitsOnlyString.append(characterToAdd)
}
else if i < originalCursorPosition {
cursorPosition -= 1
}
}
return digitsOnlyString
}
func insertCreditCardSpaces(_ string: String, preserveCursorPosition cursorPosition: inout Int) -> String {
// 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
let 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.
let is465 = [
// Amex
"34", "37",
// Diners Club
"300", "301", "302", "303", "304", "305", "309", "36", "38", "39"
].contains { string.hasPrefix($0) }
// 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.
let is4444 = !(is456 || is465)
var stringWithAddedSpaces = ""
let cursorPositionInSpacelessString = cursorPosition
for i in 0..<string.count {
let needs465Spacing = (is465 && (i == 4 || i == 10 || i == 15))
let needs456Spacing = (is456 && (i == 4 || i == 9 || i == 15))
let needs4444Spacing = (is4444 && i > 0 && (i % 4) == 0)
if needs465Spacing || needs456Spacing || needs4444Spacing {
stringWithAddedSpaces.append(" ")
if i < cursorPositionInSpacelessString {
cursorPosition += 1
}
}
let characterToAdd = string[string.index(string.startIndex, offsetBy:i)]
stringWithAddedSpaces.append(characterToAdd)
}
return stringWithAddedSpaces
}
}
Adapting this to other situations - like your delegate not being a ViewController
- is left as an exercise for the reader.
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[NSBigMutableString substringWithRange:]: Range {20, 0} out of bounds; string length 19'
–
Triangulation textField.undoManager?.removeAllActions()
at the end of reformatAsCardNumber
will stop the crash occurring. It's not a great fix but it works. –
Triangulation You can probably optimize my code or there might be an easier way but this code should work:
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
__block NSString *text = [textField text];
NSCharacterSet *characterSet = [NSCharacterSet characterSetWithCharactersInString:@"0123456789\b"];
string = [string stringByReplacingOccurrencesOfString:@" " withString:@""];
if ([string rangeOfCharacterFromSet:[characterSet invertedSet]].location != NSNotFound) {
return NO;
}
text = [text stringByReplacingCharactersInRange:range withString:string];
text = [text stringByReplacingOccurrencesOfString:@" " withString:@""];
NSString *newString = @"";
while (text.length > 0) {
NSString *subString = [text substringToIndex:MIN(text.length, 4)];
newString = [newString stringByAppendingString:subString];
if (subString.length == 4) {
newString = [newString stringByAppendingString:@" "];
}
text = [text substringFromIndex:MIN(text.length, 4)];
}
newString = [newString stringByTrimmingCharactersInSet:[characterSet invertedSet]];
if (newString.length >= 20) {
return NO;
}
[textField setText:newString];
return NO;
}
Swift 3 solution using Fawkes answer as basic. Added Amex Card format support. Added reformation when card type changed.
First make new class with this code:
extension String {
func containsOnlyDigits() -> Bool
{
let notDigits = NSCharacterSet.decimalDigits.inverted
if rangeOfCharacter(from: notDigits, options: String.CompareOptions.literal, range: nil) == nil
{
return true
}
return false
}
}
import UIKit
var creditCardFormatter : CreditCardFormatter
{
return CreditCardFormatter.sharedInstance
}
class CreditCardFormatter : NSObject
{
static let sharedInstance : CreditCardFormatter = CreditCardFormatter()
func formatToCreditCardNumber(isAmex: Bool, textField : UITextField, withPreviousTextContent previousTextContent : String?, andPreviousCursorPosition previousCursorSelection : UITextRange?) {
var selectedRangeStart = textField.endOfDocument
if textField.selectedTextRange?.start != nil {
selectedRangeStart = (textField.selectedTextRange?.start)!
}
if let textFieldText = textField.text
{
var targetCursorPosition : UInt = UInt(textField.offset(from:textField.beginningOfDocument, to: selectedRangeStart))
let cardNumberWithoutSpaces : String = removeNonDigitsFromString(string: textFieldText, andPreserveCursorPosition: &targetCursorPosition)
if cardNumberWithoutSpaces.characters.count > 19
{
textField.text = previousTextContent
textField.selectedTextRange = previousCursorSelection
return
}
var cardNumberWithSpaces = ""
if isAmex {
cardNumberWithSpaces = insertSpacesInAmexFormat(string: cardNumberWithoutSpaces, andPreserveCursorPosition: &targetCursorPosition)
}
else
{
cardNumberWithSpaces = insertSpacesIntoEvery4DigitsIntoString(string: cardNumberWithoutSpaces, andPreserveCursorPosition: &targetCursorPosition)
}
textField.text = cardNumberWithSpaces
if let finalCursorPosition = textField.position(from:textField.beginningOfDocument, offset: Int(targetCursorPosition))
{
textField.selectedTextRange = textField.textRange(from: finalCursorPosition, to: finalCursorPosition)
}
}
}
func removeNonDigitsFromString(string : String, andPreserveCursorPosition cursorPosition : inout UInt) -> String {
var digitsOnlyString : String = ""
for index in stride(from: 0, to: string.characters.count, by: 1)
{
let charToAdd : Character = Array(string.characters)[index]
if isDigit(character: charToAdd)
{
digitsOnlyString.append(charToAdd)
}
else
{
if index < Int(cursorPosition)
{
cursorPosition -= 1
}
}
}
return digitsOnlyString
}
private func isDigit(character : Character) -> Bool
{
return "\(character)".containsOnlyDigits()
}
func insertSpacesInAmexFormat(string : String, andPreserveCursorPosition cursorPosition : inout UInt) -> String {
var stringWithAddedSpaces : String = ""
for index in stride(from: 0, to: string.characters.count, by: 1)
{
if index == 4
{
stringWithAddedSpaces += " "
if index < Int(cursorPosition)
{
cursorPosition += 1
}
}
if index == 10 {
stringWithAddedSpaces += " "
if index < Int(cursorPosition)
{
cursorPosition += 1
}
}
if index < 15 {
let characterToAdd : Character = Array(string.characters)[index]
stringWithAddedSpaces.append(characterToAdd)
}
}
return stringWithAddedSpaces
}
func insertSpacesIntoEvery4DigitsIntoString(string : String, andPreserveCursorPosition cursorPosition : inout UInt) -> String {
var stringWithAddedSpaces : String = ""
for index in stride(from: 0, to: string.characters.count, by: 1)
{
if index != 0 && index % 4 == 0 && index < 16
{
stringWithAddedSpaces += " "
if index < Int(cursorPosition)
{
cursorPosition += 1
}
}
if index < 16 {
let characterToAdd : Character = Array(string.characters)[index]
stringWithAddedSpaces.append(characterToAdd)
}
}
return stringWithAddedSpaces
}
}
In your ViewControllerClass add this function
func reformatAsCardNumber(textField:UITextField){
let formatter = CreditCardFormatter()
var isAmex = false
if selectedCardType == "AMEX" {
isAmex = true
}
formatter.formatToCreditCardNumber(isAmex: isAmex, textField: textField, withPreviousTextContent: textField.text, andPreviousCursorPosition: textField.selectedTextRange)
}
Then add target to your textField
youtTextField.addTarget(self, action: #selector(self.reformatAsCardNumber(textField:)), for: UIControlEvents.editingChanged)
Register new variable and sent card type to it
var selectedCardType: String? {
didSet{
reformatAsCardNumber(textField: yourTextField)
}
}
Thanks Fawkes for his code!
1234 5678 9012
in my text field, and I position my text cursor after the 9
and hit backspace, the 9
is deleted but my text cursor ends up after the 0
rather than after the 8
. –
Leyes I think this one is good:
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
NSLog(@"%@",NSStringFromRange(range));
// Only the 16 digits + 3 spaces
if (range.location == 19) {
return NO;
}
// Backspace
if ([string length] == 0)
return YES;
if ((range.location == 4) || (range.location == 9) || (range.location == 14))
{
NSString *str = [NSString stringWithFormat:@"%@ ",textField.text];
textField.text = str;
}
return YES;
}
if ([string length] == 0) return YES;
–
Cutworm So I wanted to this with less code, so I used the code here and repurposed it a little bit. I had two fields in the screen, one for the number and one for the expiry date, so I made it more reusable.
Swift 3 alternate answer
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let currentText = (textField.text as NSString?)?.replacingCharacters(in: range, with: string) else { return true }
if textField == cardNumberTextField {
textField.text = currentText.grouping(every: 4, with: " ")
return false
}
else { // Expiry Date Text Field
textField.text = currentText.grouping(every: 2, with: "/")
return false
}
}
extension String {
func grouping(every groupSize: String.IndexDistance, with separator: Character) -> String {
let cleanedUpCopy = replacingOccurrences(of: String(separator), with: "")
return String(cleanedUpCopy.characters.enumerated().map() {
$0.offset % groupSize == 0 ? [separator, $0.element] : [$0.element]
}.joined().dropFirst())
}
}
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
{
if textField == CardNumTxt
{
let replacementStringIsLegal = string.rangeOfCharacterFromSet(NSCharacterSet(charactersInString: "0123456789").invertedSet) == nil
if !replacementStringIsLegal
{
return false
}
let newString = (textField.text! as NSString).stringByReplacingCharactersInRange(range, withString: string)
let components = newString.componentsSeparatedByCharactersInSet(NSCharacterSet(charactersInString: "0123456789").invertedSet)
let decimalString = components.joinWithSeparator("") as NSString
let length = decimalString.length
let hasLeadingOne = length > 0 && decimalString.characterAtIndex(0) == (1 as unichar)
if length == 0 || (length > 16 && !hasLeadingOne) || length > 19
{
let newLength = (textField.text! as NSString).length + (string as NSString).length - range.length as Int
return (newLength > 16) ? false : true
}
var index = 0 as Int
let formattedString = NSMutableString()
if hasLeadingOne
{
formattedString.appendString("1 ")
index += 1
}
if length - index > 4
{
let prefix = decimalString.substringWithRange(NSMakeRange(index, 4))
formattedString.appendFormat("%@-", prefix)
index += 4
}
if length - index > 4
{
let prefix = decimalString.substringWithRange(NSMakeRange(index, 4))
formattedString.appendFormat("%@-", prefix)
index += 4
}
if length - index > 4
{
let prefix = decimalString.substringWithRange(NSMakeRange(index, 4))
formattedString.appendFormat("%@-", prefix)
index += 4
}
let remainder = decimalString.substringFromIndex(index)
formattedString.appendString(remainder)
textField.text = formattedString as String
return false
}
else
{
return true
}
}
formattedString.appendFormat("%@-", prefix) chage of "-" any other your choose
In Swift 5 :
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if textField == cardNumberTextField {
return formatCardNumber(textField: textField, shouldChangeCharactersInRange: range, replacementString: string)
}
return true
}
func formatCardNumber(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
if textField == cardNumberTextField {
let replacementStringIsLegal = string.rangeOfCharacter(from: NSCharacterSet(charactersIn: "0123456789").inverted) == nil
if !replacementStringIsLegal {
return false
}
let newString = (textField.text! as NSString).replacingCharacters(in: range, with: string)
let components = newString.components(separatedBy: NSCharacterSet(charactersIn: "0123456789").inverted)
let decimalString = components.joined(separator: "") as NSString
let length = decimalString.length
let hasLeadingOne = length > 0 && decimalString.character(at: 0) == (1 as unichar)
if length == 0 || (length > 16 && !hasLeadingOne) || length > 19 {
let newLength = (textField.text! as NSString).length + (string as NSString).length - range.length as Int
return (newLength > 16) ? false : true
}
var index = 0 as Int
let formattedString = NSMutableString()
if hasLeadingOne {
formattedString.append("1 ")
index += 1
}
if length - index > 4 {
let prefix = decimalString.substring(with: NSRange(location: index, length: 4))
formattedString.appendFormat("%@ ", prefix)
index += 4
}
if length - index > 4 {
let prefix = decimalString.substring(with: NSRange(location: index, length: 4))
formattedString.appendFormat("%@ ", prefix)
index += 4
}
if length - index > 4 {
let prefix = decimalString.substring(with: NSRange(location: index, length: 4))
formattedString.appendFormat("%@ ", prefix)
index += 4
}
let remainder = decimalString.substring(from: index)
formattedString.append(remainder)
textField.text = formattedString as String
return false
} else {
return true
}
}
Here is the Swift 5 version of Mark Amery's accepted answer.
Add these variables in your class:
@IBOutlet weak var cardNumberTextField: UITextField!
private var previousTextFieldContent: String?
private var previousSelection: UITextRange?
Also ensure that your text field calls reformatAsCardNumber
: from viewDidLoad()
:
cardNumberTextField.addTarget(self, action: #selector(reformatAsCardNumber), for: .editingChanged)
Add this in your UITextFieldDelegate
:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if textField == cardNumberTextField {
previousTextFieldContent = textField.text;
previousSelection = textField.selectedTextRange;
}
return true
}
Lastly include the following methods in your viewController
:
@objc func reformatAsCardNumber(textField: UITextField) {
var targetCursorPosition = 0
if let startPosition = textField.selectedTextRange?.start {
targetCursorPosition = textField.offset(from: textField.beginningOfDocument, to: startPosition)
}
var cardNumberWithoutSpaces = ""
if let text = textField.text {
cardNumberWithoutSpaces = self.removeNonDigits(string: text, andPreserveCursorPosition: &targetCursorPosition)
}
if cardNumberWithoutSpaces.count > 19 {
textField.text = previousTextFieldContent
textField.selectedTextRange = previousSelection
return
}
let cardNumberWithSpaces = self.insertSpacesEveryFourDigitsIntoString(string: cardNumberWithoutSpaces, andPreserveCursorPosition: &targetCursorPosition)
textField.text = cardNumberWithSpaces
if let targetPosition = textField.position(from: textField.beginningOfDocument, offset: targetCursorPosition) {
textField.selectedTextRange = textField.textRange(from: targetPosition, to: targetPosition)
}
}
func removeNonDigits(string: String, andPreserveCursorPosition cursorPosition: inout Int) -> String {
var digitsOnlyString = ""
let originalCursorPosition = cursorPosition
for i in Swift.stride(from: 0, to: string.count, by: 1) {
let characterToAdd = string[string.index(string.startIndex, offsetBy: i)]
if characterToAdd >= "0" && characterToAdd <= "9" {
digitsOnlyString.append(characterToAdd)
}
else if i < originalCursorPosition {
cursorPosition -= 1
}
}
return digitsOnlyString
}
func insertSpacesEveryFourDigitsIntoString(string: String, andPreserveCursorPosition cursorPosition: inout Int) -> String {
var stringWithAddedSpaces = ""
let cursorPositionInSpacelessString = cursorPosition
for i in Swift.stride(from: 0, to: string.count, by: 1) {
if i > 0 && (i % 4) == 0 {
stringWithAddedSpaces.append(contentsOf: " ")
if i < cursorPositionInSpacelessString {
cursorPosition += 1
}
}
let characterToAdd = string[string.index(string.startIndex, offsetBy: i)]
stringWithAddedSpaces.append(characterToAdd)
}
return stringWithAddedSpaces
}
Yet another version of the accepted answer in Swift 2...
Ensure you have these in your delegate instance:
private var previousTextFieldContent: String?
private var previousSelection: UITextRange?
And also ensure that your text field calls reformatAsCardNumber:
textField.addTarget(self, action: #selector(reformatAsCardNumber(_:)), forControlEvents: .EditingChanged)
You text field delegate will need to do this:
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
previousTextFieldContent = textField.text;
previousSelection = textField.selectedTextRange;
return true
}
Lastly include the following methods:
func reformatAsCardNumber(textField: UITextField) {
var targetCursorPosition = 0
if let startPosition = textField.selectedTextRange?.start {
targetCursorPosition = textField.offsetFromPosition(textField.beginningOfDocument, toPosition: startPosition)
}
var cardNumberWithoutSpaces = ""
if let text = textField.text {
cardNumberWithoutSpaces = self.removeNonDigits(text, andPreserveCursorPosition: &targetCursorPosition)
}
if cardNumberWithoutSpaces.characters.count > 19 {
textField.text = previousTextFieldContent
textField.selectedTextRange = previousSelection
return
}
let cardNumberWithSpaces = self.insertSpacesEveryFourDigitsIntoString(cardNumberWithoutSpaces, andPreserveCursorPosition: &targetCursorPosition)
textField.text = cardNumberWithSpaces
if let targetPosition = textField.positionFromPosition(textField.beginningOfDocument, offset: targetCursorPosition) {
textField.selectedTextRange = textField.textRangeFromPosition(targetPosition, toPosition: targetPosition)
}
}
func removeNonDigits(string: String, inout andPreserveCursorPosition cursorPosition: Int) -> String {
var digitsOnlyString = ""
let originalCursorPosition = cursorPosition
for i in 0.stride(to: string.characters.count, by: 1) {
let characterToAdd = string[string.startIndex.advancedBy(i)]
if characterToAdd >= "0" && characterToAdd <= "9" {
digitsOnlyString.append(characterToAdd)
}
else if i < originalCursorPosition {
cursorPosition -= 1
}
}
return digitsOnlyString
}
func insertSpacesEveryFourDigitsIntoString(string: String, inout andPreserveCursorPosition cursorPosition: Int) -> String {
var stringWithAddedSpaces = ""
let cursorPositionInSpacelessString = cursorPosition
for i in 0.stride(to: string.characters.count, by: 1) {
if i > 0 && (i % 4) == 0 {
stringWithAddedSpaces.appendContentsOf(" ")
if i < cursorPositionInSpacelessString {
cursorPosition += 1
}
}
let characterToAdd = string[string.startIndex.advancedBy(i)]
stringWithAddedSpaces.append(characterToAdd)
}
return stringWithAddedSpaces
}
Here is a Swift version in case this is useful to anyone still looking for this answer but using Swift instead of Objective-C. The concepts are still the same regardless.
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
{
//range.length will be greater than 0 if user is deleting text - allow it to replace
if range.length > 0
{
return true
}
//Don't allow empty strings
if string == " "
{
return false
}
//Check for max length including the spacers we added
if range.location == 20
{
return false
}
var originalText = textField.text
let replacementText = string.stringByReplacingOccurrencesOfString(" ", withString: "")
//Verify entered text is a numeric value
let digits = NSCharacterSet.decimalDigitCharacterSet()
for char in replacementText.unicodeScalars
{
if !digits.longCharacterIsMember(char.value)
{
return false
}
}
//Put an empty space after every 4 places
if originalText!.length() % 5 == 0
{
originalText?.appendContentsOf(" ")
textField.text = originalText
}
return true
}
In order to achieve the goal of format the text entered in the textfield in this way XXXX XXXX XXXX XXXX is important to keep in mind some important things. Beside the fact that the 16 digits card number separated every four digit is the most common used format, there are cards with 15 digits (AmEx formatted XXXX XXXXXX XXXXX) and others with 13 digits or even with 19 digits (https://en.wikipedia.org/wiki/Payment_card_number ). Other important thing you should consider is configure the textField to allow only digits, configure the keyboard type as numberPad is a good start, but is convenient to implement a method which secure the input.
A starting point is decide when you want to format the number, while the user is entering the number or when the user leave the text field. In the case that you want to format when the user leave the textField is convenient to use the textFieldDidEndEditing(_:) delegate's method take the content of the textField and format it.
In the case you while the user is entering the number is useful the textField(_:shouldChangeCharactersIn:replacementString:) delegate method which is called whenever the current text changes.
In both cases there is still a problem, figure out which is the correct format for the entered number, IMHO and based on all the numbers that I have seen, there are only two main formats: the Amex format with 15 digits described above and the format which group card number every four digits which don not care of how much digits there are, being this case like a generic rule, for example a card with 13 digits will be formatted XXXXX XXXX XXXX X and with 19 digits will look like this XXXX XXXX XXXX XXXX XXX, this will work for the most common cases (16 digits) and for the others as well. So you could figure out how to manage the AmEx case with the same algorithm below playing with the magic numbers.
I used a RegEx to ensure that a 15 digits card is an American express, in the case of other particular formats
let regex = NSPredicate(format: "SELF MATCHES %@", "3[47][A-Za-z0-9*-]{13,}" )
let isAmex = regex.evaluate(with: stringToValidate)
I strongly recommend to use the specific RegEx which is useful to identify the Issuer and to figure out how many digits should be accepted.
Now my swift approach of the solution with textFieldDidEndEditing is
func textFieldDidEndEditing(_ textField: UITextField) {
_=format(cardNumber: textField.text!)
}
func format(cardNumber:String)->String{
var formatedCardNumber = ""
var i :Int = 0
//loop for every character
for character in cardNumber.characters{
//in case you want to replace some digits in the middle with * for security
if(i < 6 || i >= cardNumber.characters.count - 4){
formatedCardNumber = formatedCardNumber + String(character)
}else{
formatedCardNumber = formatedCardNumber + "*"
}
//insert separators every 4 spaces(magic number)
if(i == 3 || i == 7 || i == 11 || (i == 15 && cardNumber.characters.count > 16 )){
formatedCardNumber = formatedCardNumber + "-"
// could use just " " for spaces
}
i = i + 1
}
return formatedCardNumber
}
and for shouldChangeCharactersIn:replacementString: a Swift 3.0 From Jayesh Miruliya Answer, put a separator between the group of four characters
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool
{
if textField == CardNumTxt
{
let replacementStringIsLegal = string.rangeOfCharacter(from: CharacterSet(charactersIn: "0123456789").inverted) == nil
if !replacementStringIsLegal
{
return false
}
let newString = (textField.text! as NSString).replacingCharacters(in: range, with: string)
let components = newString.components(separatedBy: CharacterSet(charactersIn: "0123456789").inverted)
let decimalString = components.joined(separator: "") as NSString
let length = decimalString.length
let hasLeadingOne = length > 0 && decimalString.character(at: 0) == (1 as unichar)
if length == 0 || (length > 16 && !hasLeadingOne) || length > 19
{
let newLength = (textField.text! as NSString).length + (string as NSString).length - range.length as Int
return (newLength > 16) ? false : true
}
var index = 0 as Int
let formattedString = NSMutableString()
if hasLeadingOne
{
formattedString.append("1 ")
index += 1
}
if length - index > 4 //magic number separata every four characters
{
let prefix = decimalString.substring(with: NSMakeRange(index, 4))
formattedString.appendFormat("%@-", prefix)
index += 4
}
if length - index > 4
{
let prefix = decimalString.substring(with: NSMakeRange(index, 4))
formattedString.appendFormat("%@-", prefix)
index += 4
}
if length - index > 4
{
let prefix = decimalString.substring(with: NSMakeRange(index, 4))
formattedString.appendFormat("%@-", prefix)
index += 4
}
let remainder = decimalString.substring(from: index)
formattedString.append(remainder)
textField.text = formattedString as String
return false
}
else
{
return true
}
}
Swift 3.2
Little correction in the @Lucas answer and working code in swift 3.2. Also removing the space character automatically.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if range.location == 19 {
return false
}
if range.length == 1 {
if (range.location == 5 || range.location == 10 || range.location == 15) {
let text = textField.text ?? ""
textField.text = text.substring(to: text.index(before: text.endIndex))
}
return true
}
if (range.location == 4 || range.location == 9 || range.location == 14) {
textField.text = String(format: "%@ ", textField.text ?? "")
}
return true
}
Define below method & call it in UITextfield delegates or wherever required
-(NSString*)processString :(NSString*)yourString
{
if(yourString == nil){
return @"";
}
int stringLength = (int)[yourString length];
int len = 4; // Length after which you need to place added character
NSMutableString *str = [NSMutableString string];
int i = 0;
for (; i < stringLength; i+=len) {
NSRange range = NSMakeRange(i, len);
[str appendString:[yourString substringWithRange:range]];
if(i!=stringLength -4){
[str appendString:@" "]; //If required string format is XXXX-XXXX-XXXX-XXX then just replace [str appendString:@"-"]
}
}
if (i < [str length]-1) { // add remaining part
[str appendString:[yourString substringFromIndex:i]];
}
//Returning required string
return str;
}
Xcode 14, Swift 5.7:
Below solution should work by considering cursor situations as well. In most of the answers when we edit cursor is moved to end of field.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let textFieldText = textField.text as NSString?
let value = textFieldText?.replacingCharacters(in: range, with: string) ?? ""
textField.setText(to: value.grouping(every: 4, with: " "), preservingCursor: true)
return false
}
extension UITextField {
public func setText(to newText: String, preservingCursor: Bool) {
if preservingCursor {
let cursorPosition = offset(from: beginningOfDocument, to: selectedTextRange!.start) + newText.count - (text?.count ?? 0)
text = newText
if let newPosition = self.position(from: beginningOfDocument, offset: cursorPosition) {
selectedTextRange = textRange(from: newPosition, to: newPosition)
}}
else{
text = newText
}
}
}
extension String {
func grouping(every groupSize: String.IndexDistance, with separator: Character) -> String {
let cleanedUpCopy = replacingOccurrences(of: String(separator), with: "")
return String(cleanedUpCopy.enumerated().map() {
$0.offset % groupSize == 0 ? [separator, $0.element] : [$0.element]
}.joined().dropFirst())
}
}
Swift 3 solution based upon Mark Amery's Objective-C solution:
Implement action and delegate methods:
textField.addTarget(self, action: #selector(reformatAsCardNumber(_:))
textField.delegate = self
TextField Delegate methods and other methods:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
previousTextFieldContent = textField.text;
previousSelection = textField.selectedTextRange;
return true
}
func reformatAsCardNumber(_ textField: UITextField) {
var targetCursorPosition = 0
if let startPosition = textField.selectedTextRange?.start {
targetCursorPosition = textField.offset(from:textField.beginningOfDocument, to: startPosition)
}
var cardNumberWithoutSpaces = ""
if let text = textField.text {
cardNumberWithoutSpaces = removeNonDigits(string: text, andPreserveCursorPosition: &targetCursorPosition)
}
if cardNumberWithoutSpaces.characters.count > 19 {
textField.text = previousTextFieldContent
textField.selectedTextRange = previousSelection
return
}
let cardNumberWithSpaces = self.insertSpacesEveryFourDigitsIntoString(string: cardNumberWithoutSpaces, andPreserveCursorPosition: &targetCursorPosition)
textField.text = cardNumberWithSpaces
if let targetPosition = textField.position(from: textField.beginningOfDocument, offset: targetCursorPosition) {
textField.selectedTextRange = textField.textRange(from: targetPosition, to: targetPosition)
}
}
func removeNonDigits(string: String, andPreserveCursorPosition cursorPosition: inout Int) -> String {
var digitsOnlyString = ""
let originalCursorPosition = cursorPosition
for i in stride(from: 0, to: string.characters.count, by: 1) {
let characterToAdd = string[string.index(string.startIndex, offsetBy: i)]
if characterToAdd >= "0" && characterToAdd <= "9" {
digitsOnlyString.append(characterToAdd)
}
else if i < originalCursorPosition {
cursorPosition -= 1
}
}
return digitsOnlyString
}
func insertSpacesEveryFourDigitsIntoString(string: String, andPreserveCursorPosition cursorPosition: inout Int) -> String {
var stringWithAddedSpaces = ""
let cursorPositionInSpacelessString = cursorPosition
for i in stride(from: 0, to: string.characters.count, by: 1) {
if i > 0 && (i % 4) == 0 {
stringWithAddedSpaces.append(" ")
if i < cursorPositionInSpacelessString {
cursorPosition += 1
}
}
let characterToAdd = string[string.index(string.startIndex, offsetBy: i)]
stringWithAddedSpaces.append(characterToAdd)
}
return stringWithAddedSpaces
}
Swift 5.1, Xcode 11
After trying many solutions, I faced issues such as setting correct cursor position and formating as per need, I finally found a solution after combining 2 posts (https://mcmap.net/q/258955/-formatting-a-uitextfield-for-credit-card-input-like-xxxx-xxxx-xxxx-xxxx, https://mcmap.net/q/258955/-formatting-a-uitextfield-for-credit-card-input-like-xxxx-xxxx-xxxx-xxxx)
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let currentText = (textField.text as NSString?)?.replacingCharacters(in: range, with: string) else { return true }
if textField == yourTextField {
textField.setText(to: currentText.grouping(every: 4, with: "-"), preservingCursor: true)
return false
}
return true
}
And adding this extension
extension UITextField {
public func setText(to newText: String, preservingCursor: Bool) {
if preservingCursor {
let cursorPosition = offset(from: beginningOfDocument, to: selectedTextRange!.start) + newText.count - (text?.count ?? 0)
text = newText
if let newPosition = self.position(from: beginningOfDocument, offset: cursorPosition) {
selectedTextRange = textRange(from: newPosition, to: newPosition)
}
}
else {
text = newText
}
}
Here's a swift copy of the accepted answer. It is basically a wrapper class:
var creditCardFormatter : CreditCardFormatter
{
return CreditCardFormatter.sharedInstance
}
class CreditCardFormatter : NSObject
{
static let sharedInstance : CreditCardFormatter = CreditCardFormatter()
func formatToCreditCardNumber(textField : UITextField, withPreviousTextContent previousTextContent : String?, andPreviousCursorPosition previousCursorSelection : UITextRange?)
{
if let selectedRangeStart = textField.selectedTextRange?.start, textFieldText = textField.text
{
var targetCursorPosition : UInt = UInt(textField.offsetFromPosition(textField.beginningOfDocument, toPosition: selectedRangeStart))
let cardNumberWithoutSpaces : String = removeNonDigitsFromString(textFieldText, andPreserveCursorPosition: &targetCursorPosition)
if cardNumberWithoutSpaces.characters.count > 19
{
textField.text = previousTextContent
textField.selectedTextRange = previousCursorSelection
return
}
let cardNumberWithSpaces : String = insertSpacesIntoEvery4DigitsIntoString(cardNumberWithoutSpaces, andPreserveCursorPosition: &targetCursorPosition)
textField.text = cardNumberWithSpaces
if let finalCursorPosition = textField.positionFromPosition(textField.beginningOfDocument, offset: Int(targetCursorPosition))
{
textField.selectedTextRange = textField.textRangeFromPosition(finalCursorPosition, toPosition: finalCursorPosition)
}
}
}
func removeNonDigitsFromString(string : String,inout andPreserveCursorPosition cursorPosition : UInt) -> String
{
var digitsOnlyString : String = ""
for index in 0.stride(to: string.characters.count, by: 1)
{
let charToAdd : Character = Array(string.characters)[index]
if isDigit(charToAdd)
{
digitsOnlyString.append(charToAdd)
}
else
{
if index < Int(cursorPosition)
{
cursorPosition -= 1
}
}
}
return digitsOnlyString
}
private func isDigit(character : Character) -> Bool
{
return "\(character)".containsOnlyDigits()
}
func insertSpacesIntoEvery4DigitsIntoString(string : String, inout andPreserveCursorPosition cursorPosition : UInt) -> String
{
var stringWithAddedSpaces : String = ""
for index in 0.stride(to: string.characters.count, by: 1)
{
if index != 0 && index % 4 == 0
{
stringWithAddedSpaces += " "
if index < Int(cursorPosition)
{
cursorPosition += 1
}
}
let characterToAdd : Character = Array(string.characters)[index]
stringWithAddedSpaces.append(characterToAdd)
}
return stringWithAddedSpaces
}
}
extension String
{
func containsOnlyDigits() -> Bool
{
let notDigits : NSCharacterSet = NSCharacterSet.decimalDigitCharacterSet().invertedSet
if (rangeOfCharacterFromSet(notDigits, options: NSStringCompareOptions.LiteralSearch, range: nil) == nil)
{
return true
}
return false
}
}
previousTextContent
anywhere, so it receives nil
(or, if you make it a String
instead of a String?
, receives some random garbage bytes). That means that if you overflow 19 characters, the whole text field just gets blanked (or perhaps the app crashes outright if you're unlucky - but so far I've always seen the blanking). –
Leyes Here is a Kotlin answer based off of Mark Amery:
fun formatCardNumber(cardNumber: String): String {
var trimmedCardNumber = cardNumber.replace(" ","")
// UATP cards have 4-5-6 (XXXX-XXXXX-XXXXXX) format
val is456 = trimmedCardNumber.startsWith("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.
val is465 = listOf("34", "37", "300", "301", "302", "303", "304", "305", "309", "36", "38", "39")
.any { trimmedCardNumber.startsWith(it) }
// In all other cases, assume 4-4-4-4.
val is4444 = !(is456 || is465)
trimmedCardNumber = if (is456 || is465) {
trimmedCardNumber.take(cardNumberMaxLengthAmex)
} else {
trimmedCardNumber.take(cardNumberMaxLength)
}
var cardNumberWithAddedSpaces = ""
trimmedCardNumber.forEachIndexed { index, c ->
val needs465Spacing = is465 && (index == 4 || index == 10 || index == 15)
val needs456Spacing = is456 && (index == 4 || index == 9 || index == 15)
val needs4444Spacing = is4444 && index > 0 && index % 4 == 0
if (needs465Spacing || needs456Spacing || needs4444Spacing) {
cardNumberWithAddedSpaces += " "
}
cardNumberWithAddedSpaces += c
}
return cardNumberWithAddedSpaces
}
Then add a text changed listener on an edit text:
var flag = false
editText.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
if (flag) {
flag = false
} else {
val text = formatCardNumber(s.toString())
flag = true
editText.setText(text)
editText.setSelection(text.count())
}
}
override fun afterTextChanged(s: Editable?) {}
})
You can use my simple library: DECardNumberFormatter
Example:
// You can use it like default UITextField
let textField = DECardNumberTextField()
// Custom required setup
textField.setup()
Output:
For sample card number (Visa) 4111111111111111
Format (4-4-4-4): 4111 1111 1111 1111
For sample card number (AmEx) 341212345612345
Format (4-6-5): 3412 123456 12345
Check Out This Solution. I found in Autorize.net SDK Example.
Make Your UITextField
Keyboard Type to Numeric
.
It Will Mask Credit Card Numbers With 'X' And By Adding Spaces It Will Make 'XXXX XXXX XXXX 1234'
format.
In Header .h file
#define kSpace @" "
#define kCreditCardLength 16
#define kCreditCardLengthPlusSpaces (kCreditCardLength + 3)
#define kCreditCardObscureLength (kCreditCardLength - 4)
@property (nonatomic, strong) NSString *creditCardBuf;
IBOutlet UITextField *txtCardNumber;
In .m file
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
{
if (textField == txtCardNumber) {
if ([string length] > 0) { //NOT A BACK SPACE Add it
if ([self isMaxLength:textField])
return NO;
self.creditCardBuf = [NSString stringWithFormat:@"%@%@", self.creditCardBuf, string];
} else {
//Back Space do manual backspace
if ([self.creditCardBuf length] > 1) {
self.creditCardBuf = [self.creditCardBuf substringWithRange:NSMakeRange(0, [self.creditCardBuf length] - 1)];
} else {
self.creditCardBuf = @"";
}
}
[self formatValue:textField];
}
return NO;
}
- (BOOL) isMaxLength:(UITextField *)textField {
if (textField == txtCardNumber && [textField.text length] >= kCreditCardLengthPlusSpaces) {
return YES;
}
return NO;
}
- (void) formatValue:(UITextField *)textField {
NSMutableString *value = [NSMutableString string];
if (textField == txtCardNumber) {
NSInteger length = [self.creditCardBuf length];
for (int i = 0; i < length; i++) {
// Reveal only the last character.
if (length <= kCreditCardObscureLength) {
if (i == (length - 1)) {
[value appendString:[self.creditCardBuf substringWithRange:NSMakeRange(i,1)]];
} else {
[value appendString:@“X”];
}
}
// Reveal the last 4 characters
else {
if (i < kCreditCardObscureLength) {
[value appendString:@“X”];
} else {
[value appendString:[self.creditCardBuf substringWithRange:NSMakeRange(i,1)]];
}
}
//After 4 characters add a space
if ((i +1) % 4 == 0 &&
([value length] < kCreditCardLengthPlusSpaces)) {
[value appendString:kSpace];
}
}
textField.text = value;
}
}
X
s) the string (nul l)
into the start of the text field, and if I type a character in the middle of the card number, it jumps my cursor to the end. –
Leyes Here's a solution in Swift 2.2.1
extension UITextField {
func setText(to newText: String, preservingCursor: Bool) {
if preservingCursor {
let cursorPosition = offsetFromPosition(beginningOfDocument, toPosition: selectedTextRange!.start) + newText.characters.count - (text?.characters.count ?? 0)
text = newText
if let newPosition = positionFromPosition(beginningOfDocument, offset: cursorPosition) {
selectedTextRange = textRangeFromPosition(newPosition, toPosition: newPosition)
}
}
else {
text = newText
}
}
}
Now just put an IBAction in your view controller:
@IBAction func textFieldEditingChanged(sender: UITextField) {
var digits = current.componentsSeparatedByCharactersInSet(NSCharacterSet.decimalDigitCharacterSet().invertedSet).joinWithSeparator("") // remove non-digits
// add spaces as necessary or otherwise format your digits.
// for example for a phone number or zip code or whatever
// then just:
sender.setText(to: digits, preservingCursor: true)
}
current
meant to be?), this doesn't preserve the cursor position properly. e.g. if I have 1234 5678
in the text field and type a 0
after the 4
, I end up with 1234 5678 0
but with my cursor before the 0
I just typed, instead of after it. –
Leyes Please use simple form of credite card /** See sample usage: ### let str = "41111111111111111"
let x = yourClassname.setStringAsCardNumberWithSartNumber(4, withString: str!, withStrLenght: 8)
### output:- 4111XXXXXXXX1111
let x = yourClassname.setStringAsCardNumberWithSartNumber(0, withString: str!, withStrLenght: 12)
### output: - XXXXXXXXXXXX1111
*/
func setStringAsCardNumberWithSartNumber(Number:Int,withString str:String ,withStrLenght len:Int ) -> String{
//let aString: String = "41111111111111111"
let arr = str.characters
var CrediteCard : String = ""
if arr.count > (Number + len) {
for (index, element ) in arr.enumerate(){
if index >= Number && index < (Number + len) {
CrediteCard = CrediteCard + String("X")
}else{
CrediteCard = CrediteCard + String(element)
}
}
return CrediteCard
}else{
print("\(Number) plus \(len) are grether than strings chatarter \(arr.count)")
}
print("\(CrediteCard)")
return str
}
Please check bellow solution, its working fine for me-
func textField(textField: UITextField, shouldChangeCharactersInRange range: NSRange, replacementString string: String) -> Bool {
let subString = (textField.text as! NSString).substringWithRange(range)
if subString == " " && textField == cardNumberTextfield
{
return false // user should not be able to delete space from card field
}
else if string == ""
{
return true // user can delete any digit
}
// Expiry date formatting
if textField == expiryDateTextfield
{
let str = textField.text! + string
if str.length == 2 && Int(str) > 12
{
return false // Month should be <= 12
}
else if str.length == 2
{
textField.text = str+"/" // append / after month
return false
}
else if str.length > 5
{
return false // year should be in yy format
}
}
// Card number formatting
if textField == cardNumberTextfield
{
let str = textField.text! + string
let stringWithoutSpace = str.stringByReplacingOccurrencesOfString(" ", withString: "")
if stringWithoutSpace.length % 4 == 0 && (range.location == textField.text?.length)
{
if stringWithoutSpace.length != 16
{
textField.text = str+" " // add space after every 4 characters
}
else
{
textField.text = str // space should not be appended with last digit
}
return false
}
else if str.length > 19
{
return false
}
}
return true
}
i modified @ilesh answer so it only shows the last 4 digits no matter what the lenght is. Also to ignore the space and "-" chars. This way, if we have a number with the format 0000 - 0000 - 0000 - 0000 it displays XXXX - XXXX - XXXX - 0000
func setStringAsCardNumberWithSartNumber(Number:Int,withString str:String) -> String{
let arr = str.characters
var CrediteCard : String = ""
let len = str.characters.count-4
if arr.count > (Number + len) {
for (index, element ) in arr.enumerated(){
if index >= Number && index < (Number + len) && element != "-" && element != " " {
CrediteCard = CrediteCard + String("X")
}else{
CrediteCard = CrediteCard + String(element)
}
}
return CrediteCard
}else{
print("\(Number) plus \(len) are grether than strings chatarter \(arr.count)")
}
print("\(CrediteCard)")
return str
}
UITextField
s. –
Leyes Here is the modification of the answer from @sleeping_giant for swift. This solution formats the text in xxxx-xxxx-xxxx-xxxx-xxxx
format and stops accepting any numbers beyond that range:
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool
{
if string == ""{
return true
}
//range.length will be greater than 0 if user is deleting text - allow it to replace
if range.length > 0
{
return true
}
//Don't allow empty strings
if string == "-"
{
return false
}
//Check for max length including the spacers we added
print(range.location)
if range.location > 23
{
return false
}
var originalText = textField.text
let replacementText = string.replacingOccurrences(of: "-", with: "")
//Verify entered text is a numeric value
let digits = NSCharacterSet.decimalDigits
for char in replacementText.unicodeScalars
{
if !(digits as NSCharacterSet).longCharacterIsMember(char.value)
{
return false
}
}
//Put an empty space after every 4 places
if (originalText?.characters.count)! > 0
{
if (originalText?.characters.count)! < 5 && (originalText?.characters.count)! % 4 == 0{
originalText?.append("-")
}else if(((originalText?.characters.count)! + 1) % 5 == 0){
originalText?.append("-")
}
}
textField.text = originalText
return true
}
Found a GIST in Github that does exactly what I need in Swift3 (https://gist.github.com/nunogoncalves/6a8b4b21f4f69e0fc050190df96a1e56)
Implemented by doing ->
if creditCardNumberTextView.text?.characters.first == "3" {
let validator = Validator(cardType: .americanExpress, value: self.creditCardNumberTextView.text!).test()
if validator == true {
} else {
}
}
Works wonderfully in the APP I'm working out which uses credit cards.
In my case, we have to formatting IBAN number. I think, the below code block help you
Firstly, check the user enterted value is valid:
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string{
if(textField == self.ibanTextField){
BOOL shouldChange = ([Help checkTextFieldForIBAN:[NSString stringWithFormat:@"%@%@",textField.text,string]]);
}
}
Secondly, you can see iban formated method just like below. Our IBAN formatted begin 2 letter.
+(BOOL)checkTextFieldForIBAN:(NSString*)string{
string = [string stringByReplacingOccurrencesOfString:@" " withString:@""];
if ([string length] <= 26) {
if ([string length] > 2) {
if ([self isLetter:[string substringToIndex:2]]) {
if ([self isInteger:[string substringFromIndex:2]])
return YES;
else
return NO;
}else {
return NO;
}
}else{
return [self isLetter:string];
}
}
else {
return NO;
}
return YES;
}
Create new swift file and paste below code, change text field class to VSTextField
import UIKit
public enum TextFieldFormatting {
case uuid
case socialSecurityNumber
case phoneNumber
case custom
case noFormatting
}
public class VSTextField: UITextField {
/**
Set a formatting pattern for a number and define a replacement string. For example: If formattingPattern would be "##-##-AB-##" and
replacement string would be "#" and user input would be "123456", final string would look like "12-34-AB-56"
*/
public func setFormatting(_ formattingPattern: String, replacementChar: Character) {
self.formattingPattern = formattingPattern
self.replacementChar = replacementChar
self.formatting = .custom
}
/**
A character which will be replaced in formattingPattern by a number
*/
public var replacementChar: Character = "*"
/**
A character which will be replaced in formattingPattern by a number
*/
public var secureTextReplacementChar: Character = "\u{25cf}"
/**
True if input number is hexadecimal eg. UUID
*/
public var isHexadecimal: Bool {
return formatting == .uuid
}
/**
Max length of input string. You don't have to set this if you set formattingPattern.
If 0 -> no limit.
*/
public var maxLength = 0
/**
Type of predefined text formatting. (You don't have to set this. It's more a future feature)
*/
public var formatting : TextFieldFormatting = .noFormatting {
didSet {
switch formatting {
case .socialSecurityNumber:
self.formattingPattern = "***-**-****"
self.replacementChar = "*"
case .phoneNumber:
self.formattingPattern = "***-***-****"
self.replacementChar = "*"
case .uuid:
self.formattingPattern = "********-****-****-****-************"
self.replacementChar = "*"
default:
self.maxLength = 0
}
}
}
/**
String with formatting pattern for the text field.
*/
public var formattingPattern: String = "" {
didSet {
self.maxLength = formattingPattern.count
}
}
/**
Provides secure text entry but KEEPS formatting. All digits are replaced with the bullet character \u{25cf} .
*/
public var formatedSecureTextEntry: Bool {
set {
_formatedSecureTextEntry = newValue
super.isSecureTextEntry = false
}
get {
return _formatedSecureTextEntry
}
}
override public var text: String! {
set {
super.text = newValue
textDidChange() // format string properly even when it's set programatically
}
get {
if case .noFormatting = formatting {
return super.text
} else {
// Because the UIControl target action is called before NSNotificaion (from which we fire our custom formatting), we need to
// force update finalStringWithoutFormatting to get the latest text. Otherwise, the last character would be missing.
textDidChange()
return finalStringWithoutFormatting
}
}
}
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
registerForNotifications()
}
override init(frame: CGRect) {
super.init(frame: frame)
registerForNotifications()
}
deinit {
NotificationCenter.default.removeObserver(self)
}
/**
Final text without formatting characters (read-only)
*/
public var finalStringWithoutFormatting : String {
return _textWithoutSecureBullets.keepOnlyDigits(isHexadecimal: isHexadecimal)
}
// MARK: - INTERNAL
fileprivate var _formatedSecureTextEntry = false
// if secureTextEntry is false, this value is similar to self.text
// if secureTextEntry is true, you can find final formatted text without bullets here
fileprivate var _textWithoutSecureBullets = ""
fileprivate func registerForNotifications() {
NotificationCenter.default.addObserver(self,
selector: #selector(VSTextField.textDidChange),
name: NSNotification.Name(rawValue: "UITextFieldTextDidChangeNotification"),
object: self)
}
@objc public func textDidChange() {
var superText: String { return super.text ?? "" }
// TODO: - Isn't there more elegant way how to do this?
let currentTextForFormatting: String
if superText.count > _textWithoutSecureBullets.count {
currentTextForFormatting = _textWithoutSecureBullets + superText[superText.index(superText.startIndex, offsetBy: _textWithoutSecureBullets.count)...]
} else if superText.count == 0 {
_textWithoutSecureBullets = ""
currentTextForFormatting = ""
} else {
currentTextForFormatting = String(_textWithoutSecureBullets[..<_textWithoutSecureBullets.index(_textWithoutSecureBullets.startIndex, offsetBy: superText.count)])
}
if formatting != .noFormatting && currentTextForFormatting.count > 0 && formattingPattern.count > 0 {
let tempString = currentTextForFormatting.keepOnlyDigits(isHexadecimal: isHexadecimal)
var finalText = ""
var finalSecureText = ""
var stop = false
var formatterIndex = formattingPattern.startIndex
var tempIndex = tempString.startIndex
while !stop {
let formattingPatternRange = formatterIndex ..< formattingPattern.index(formatterIndex, offsetBy: 1)
if formattingPattern[formattingPatternRange] != String(replacementChar) {
finalText = finalText + formattingPattern[formattingPatternRange]
finalSecureText = finalSecureText + formattingPattern[formattingPatternRange]
} else if tempString.count > 0 {
let pureStringRange = tempIndex ..< tempString.index(tempIndex, offsetBy: 1)
finalText = finalText + tempString[pureStringRange]
// we want the last number to be visible
if tempString.index(tempIndex, offsetBy: 1) == tempString.endIndex {
finalSecureText = finalSecureText + tempString[pureStringRange]
} else {
finalSecureText = finalSecureText + String(secureTextReplacementChar)
}
tempIndex = tempString.index(after: tempIndex)
}
formatterIndex = formattingPattern.index(after: formatterIndex)
if formatterIndex >= formattingPattern.endIndex || tempIndex >= tempString.endIndex {
stop = true
}
}
_textWithoutSecureBullets = finalText
let newText = _formatedSecureTextEntry ? finalSecureText : finalText
if newText != superText {
super.text = _formatedSecureTextEntry ? finalSecureText : finalText
}
}
// Let's check if we have additional max length restrictions
if maxLength > 0 {
if superText.count > maxLength {
super.text = String(superText[..<superText.index(superText.startIndex, offsetBy: maxLength)])
_textWithoutSecureBullets = String(_textWithoutSecureBullets[..<_textWithoutSecureBullets.index(_textWithoutSecureBullets.startIndex, offsetBy: maxLength)])
}
}
}
}
extension String {
func keepOnlyDigits(isHexadecimal: Bool) -> String {
let ucString = self.uppercased()
let validCharacters = isHexadecimal ? "0123456789ABCDEF" : "0123456789"
let characterSet: CharacterSet = CharacterSet(charactersIn: validCharacters)
let stringArray = ucString.components(separatedBy: characterSet.inverted)
let allNumbers = stringArray.joined(separator: "")
return allNumbers
}
}
// Helpers
fileprivate func < <T: Comparable>(lhs: T?, rhs: T?) -> Bool {
switch (lhs, rhs) {
case let (l?, r?):
return l < r
case (nil, _?):
return true
default:
return false
}
}
fileprivate func > <T: Comparable>(lhs: T?, rhs: T?) -> Bool {
switch (lhs, rhs) {
case let (l?, r?):
return l > r
default:
return rhs < lhs
}
}
More uses will be found on below links:
You can use StringPatternFormatter
pod:
pod 'StringPatternFormatter'
The UITextField
need to implement the following method from UITextFieldDelegate
:
import StringPatternFormatter
...
textField.delegate = self
...
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
guard let text = textField.text else {
return true
}
let lastText = (text as NSString).replacingCharacters(in: range, with: string) as String
textField.text = lastText.format("nnnn nnnn nnnn nnnn", oldString: text)
return false
}
Answer of @jayant give me base for what I was looking for, however there is no limitation for card length which is 16.
So updated answer with 16 digit card number is as below.
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
let textFieldText = textField.text as NSString?
let value = textFieldText?.replacingCharacters(in: range, with: string) ?? ""
// here is update I make
// so what I check if the new text is beyond 16 (after removing spaces),
// use textfield text as it is else use new text
let finalText : String = ((value.replacingOccurrences(of: " ", with: "").count > 16) ? (textFieldText ?? "") as String : value)
textField.setText(to: finalText.grouping(every: 4, with: " "), preservingCursor: true)
return false
}
Rest code as above
extension UITextField {
public func setText(to newText: String, preservingCursor: Bool) {
if preservingCursor {
let cursorPosition = offset(from: beginningOfDocument, to: selectedTextRange!.start) + newText.count - (text?.count ?? 0)
text = newText
if let newPosition = self.position(from: beginningOfDocument, offset: cursorPosition) {
selectedTextRange = textRange(from: newPosition, to: newPosition)
}}
else{
text = newText
}
}
}
extension String {
func grouping(every groupSize: String.IndexDistance, with separator: Character) -> String {
let cleanedUpCopy = replacingOccurrences(of: String(separator), with: "")
return String(cleanedUpCopy.enumerated().map() {
$0.offset % groupSize == 0 ? [separator, $0.element] : [$0.element]
}.joined().dropFirst())
}
}
© 2022 - 2024 — McMap. All rights reserved.