Credit card format masking (xxxx xxxx xxxx xxxx)
Introduction
Recently - actually around a week ago - I was tasked to find a way to do certain format manipulation to a credit card field textfield. It was more of a decorative purpose than anything else, but he just wanted the credit card field to have 16 numbers, with space between each pair of 4 numbers. You know, rather than 4111111111111111, have 4111 1111 1111 1111. It would be used for the Merchant Hosted payment page, where the controls for users to type up their details (expiry date, cvc, card number) would be through the native module, rather than on the redirected page as would be the case for HPP (hosted payment page).
After searching quite a bit of documentation, understanding:
- how to make a text field
- what is a delegate
- IBOutlet (TextField view) / IBAction (callbacks associated with TextField view)
- textField:shouldChangeCharactersInRange:replacementString, viewDidLoad, UIKit>Views and Controls>UITextField
I finally could manage to come to the following result:
I recommend firstly reading up: stackoverflow.com/a/15103112, apple documentation called Concepts in Objective-C Programming (which talks about delegate and outlet), Tutorials Point iOS (pages: iOS - Delegates, iOS - UI Elements), and other stackoverflow/google articles to get a general understanding of the terms involved. Some concepts would be clear, and some would seem very vague. Youtube videos are generally unhelpful because of how outdated they are.
It's also worth noting the meaning of 'did' 'should' 'will' 'has'. I recall seeing those in React, but it's also here. Apple documentation actually explains these terms briefly:
The page is Delegate and Data Sources, and it's worth reading carefully.
Anyhow, let's get started!
Practice task: Log something when textfield is pressed
Create a new project under iOS, with Single Page Application preset.
You shouldn't have to worry about AppDelegate and SceneDelegate, I think AppDelegate is just where the application is launched, and SceneDelegate I'm not so sure about. ViewController and Main.storyboard are what we are concerned with the most.
Go on Main.storyboard. Click '+' button, and drag the textfield into the screen. You can position it wherever you like.
Now we would like ViewController to be able to refer to that text field. To create this reference link easily, firstly set up the IDE so that storyboard shows up on the left, and ViewController.m shows up on the right. The way to do this is by clicking the button below:
Then just navigating the right pane to ViewController.m
Now, here's a very convenient way to link the textfield with the ViewController. Click textfield with right mouse button, then drag it to right under @interface. Then you get the window like so:
I called it creditTextField.
The result of it is a property called creditTextField. You can look up what IBOutlet and property mean, but for all our purpose, we just understand it as a field (or variable) that points to (refers to) the textfield on the left. IBOutlet is an umbrella type for anything to do with graphical component that we see on the screen. You wouldn't be able to do this if you drag to under @implementation, so you don't have to memorize that it has to be dragged to under @interface.
How do we cause something to happen when the GUI component (in our case, Text Field) is interacted in certain ways? We can define these behaviours by clicking the text field with right button, dragging it to under @property that we added (well, as long as it's within @interface declaration, it's fine), then instead of Connection type of Outlet, change it to Action on dropdown.
Here, we are adding a method signature that deals with the case when Editing Did Begin event happens, and the name of the method is arbitrary.
A useful thing to check at this point is the above screen. You get this window by right clicking the text field on the storyboard. You can see that under Sent Events, Editing Did Begin is linked to a method soDoThis... method, and it is linked to the Outlet called creditTextField in ViewController. Implement soDoThisWhenEditStarts (which is triggered whenever the textfield starts being editted) to invoke a simple indication like NSLog(@"Hello!");, and see that it successfully gets called when the textfield is pressed.
Disallow modification of textfield
As you would have noticed, that's not exactly what we were trying to do! We want to somehow force the textfield to accept inputs in certain way, and format what we see in certain way. Well, apple puts it quite nicely:
(developer.apple.com/documentation/uikit/uitextfield?language=objc)
So we need to use a delegate... and I told you to read over the delegate, so please do if you have time. But essentially, we are going to make our ViewController to be a delegate of the textfield (to be specific, we want to make ViewController abide by a protocol called UITextFieldDelegate - what is a protocol? it's equivalent to interface in java, and specifies what methods whatever is subclassing that protocol is going to provide implementations for certain methods). ViewController can override some default implementations of the UITextFieldDelegate protocol, and one of the method, called textField:shouldChangeCharactersInRange:replacementString is in charge of handling the input and validating it.
The terms 'delegate' and 'protocol' must be programmatically concrete in your head, otherwise I'd recommend reading documentation on it.
We start with something simple, so that we understand how things are linked together - we are going to limit the entry of any characters to the field (notice we are not disabling the textfield)
Firstly, go on ViewController.h
This is what you see at the start. But we want the ViewController to act as a delegate for the textfield - that is, we want to make the ViewController such that it implements certain behaviour of the textfield. developer.apple.com/documentation/uikit/uitextfielddelegate?language=objc details methods that ViewController abiding by the UITextFieldProtocol can implement (all of them are optional to implement).
Then, go on ViewController.m to implement that method
By returning NO, you can check that you can't enter anything on the simulation. Oh, also on line 29, you see that I added a line. That line adds ViewController (that now abides by UITextFieldDelegate protocol) to the delegate field of the creditTextField, which is what we want to do. Why under viewDidLoad? because that will be called as soon as the screen loads up.
You can also change the method implementation of textField:shouldChangeCharactersInRange: so that it returns YES when the textfield length is less than 5, and NO otherwise (if(creditTextField.text.length < 5){ return YES; } return NO;) - this will prevent any entry after enterring 5 letters.
You can play around with it to understand more about what the parameters indicate. For example, if you NSLog() the range parameter, you can see that the first number of the range indicates the location at which change will be applied, second number is how many characters are to be deleted (if 0, we are adding characters) - notice you can use NSStringFromRange to convert NSRange to readable string. string parameter indicates what letters will be added.
Credit card format masking in objective c
Now we are onto the last stage. The to-do here is really simple. Just simply copy and paste the following code into ViewController.m under @implementation. Quite complicated logic, but it should be fairly clear to you how things link together.
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {
// NSString *filter = @"(###) ### - ####";
NSString *filter = @"#### #### #### ####";
if(!filter) return YES; // No filter provided, allow anything
NSString *changedString = [textField.text stringByReplacingCharactersInRange:range withString:string];
if(range.length == 1 && // Only do for single deletes
string.length < range.length &&
[[textField.text substringWithRange:range] rangeOfCharacterFromSet:[NSCharacterSet characterSetWithCharactersInString:@"0123456789"]].location == NSNotFound)
{
// Something was deleted. Delete past the previous number
NSInteger location = changedString.length-1;
if(location > 0)
{
for(; location > 0; location--)
{
if(isdigit([changedString characterAtIndex:location]))
{
break;
}
}
changedString = [changedString substringToIndex:location];
}
}
NSLog(@"%@",filteredPhoneStringFromStringWithFilter(changedString, filter));
textField.text = filteredPhoneStringFromStringWithFilter(changedString, filter);
return NO;
}
NSString *filteredPhoneStringFromStringWithFilter(NSString *string, NSString *filter)
{
NSUInteger onOriginal = 0, onFilter = 0, onOutput = 0;
char outputString[([filter length])];
BOOL done = NO;
while(onFilter < [filter length] && !done)
{
char filterChar = [filter characterAtIndex:onFilter];
char originalChar = onOriginal >= string.length ? '\0' : [string characterAtIndex:onOriginal];
switch (filterChar) {
case '#':
if(originalChar=='\0')
{
// We have no more input numbers for the filter. We're done.
done = YES;
break;
}
if(isdigit(originalChar))
{
outputString[onOutput] = originalChar;
onOriginal++;
onFilter++;
onOutput++;
}
else
{
onOriginal++;
}
break;
default:
// Any other character will automatically be inserted for the user as they type (spaces, - etc..) or deleted as they delete if there are more numbers to come.
outputString[onOutput] = filterChar;
onOutput++;
onFilter++;
if(originalChar == filterChar)
onOriginal++;
break;
}
}
outputString[onOutput] = '\0'; // Cap the output string
return [NSString stringWithUTF8String:outputString];
}
That's it!