My belief is that a text field should be able to do its own validation. Unfortunately, the delegate pattern causes many people to believe they must put the validation logic in a UIViewController, and I don’t blame people for thinking this. I was doing iOS development for well over a year before I finally started to question why.
Previously, I did a post titled UITextField Max Length (The Right Way). That post is similar, and uses some of the same concepts and ideas. But I decided to do another writeup on the topic where I solve a different problem in two different ways. The max length post is great for understanding the nitty gritty theory of what we’re doing, but it doesn’t do too well on how to use the knowledge in a practical manner. So I hope this helps remedy the situation.
Problem Explanation
We’re going to create a text field that only accepts valid positive integers to be entered. We will not allow any kind of negative number, or any type of character. It will also not allow the user to paste invalid data (they can try to paste, but the text will be rejected). This will ensure the text field is always valid.
Validation logic can get complicated fast, but I’d rather focus on how to create a self validating text field. I’m not really interested in doing a writeup of how to decide if your validation logic is good or not. For this reason, our validation logic will be very simple. It will not allow grouping characters (, or . depending on your culture) or negative numbers. Nothing against those things, they just make the validation code more complicated, and that takes focus away from the core of this post.
The most important thing to note is that, a text field can not be its own delegate. Considering the whole purpose of this post is to make a text field do its own validation, you would think the goto solution would be “make the text field be its own delegate.” But unfortunately you can’t. If you try to make a text field be its own delegate, it will result in an infinite recursion of respondsToSelector:. It is entirely Apples code that causes this problem. I guess that means this post should be titled “sort of self validating text fields,” but that’s just a mouth full.
Because we can’t have the text field be its own delegate, we will create an NSObject to encapsulate the delegate logic, and then have that delegate be owned by the text field.1 This gets the logic out of the view controller, and into a reusable object owned by the text field. In the end, we’ll be able to put a QuantityField on any page, without having to put validation logic in every view controller we can think of.
There are two ways of handling the delegate logic. We can solve the problem with surrogates (wrappers), or blocks. Both of these solutions work by replacing the single delegate of the text field with a delegate chain. The only difference between the solutions is how you insert the validation logic into this new delegate chain.
Surrogates uses some advanced objective-c runtime mechanics to make for less boiler plate code, but Blocks can result in less files overall. Both ways work just fine and neither way is better than the other. It all depends on the person writing code, and the problem being solved. You can decide for yourself which you prefer.
Solution Overview
he normal relationship with a text field and its delegate is illustrated like this.
In this example, the text field delegates certain responsibilities to the view controller. Much like a manager delegates responsibilities to their underlings.
But we are going to create a system like this
In this system, we the manager delegates certain responsibilities to an underling, and the underling delegates certain responsibilities to the intern. Because the work goes through the underling first, he can decide what to do
- Send the work to the intern
- Do some work, and send the rest to the intern
- Have the intern do some work, but check the results before taking credit from the intern
Now that we have a base knowledge of the system we want to create, we can implement a solution using either a Surrogate or Blocks approach.
Surrogate
To create the desired system, you’ll want to include this class in your project
BaseSurrogateDelegate.h BaseBlockDelegate.m
The class provided is just boiler plate code. The code was created based on Apple’s documentation. The class provides a foundation for message forwarding. This message forwarding system is what allows the underling to do some stuff, and send the rest to the intern.
The steps we will take to solve the problem are
- Create a subclass of
BaseSurrogateDelegate
- the validation logic goes in here
- Create a subclass of
UITextField
- init the subclass of BaseSurrogateDelegate
We start off by creating our subclass of BaseSurrogateDelegate
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
//header file #import "BaseSurrogateDelegate.h" @interface QuantityDelegate : BaseSurrogateDelegate @end //implementation file @implementation QuantityDelegate //implement the UITextFieldDelegate method required -(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { BOOL returnValue = YES; NSString* tmp = [self.text stringByReplacingCharactersInRange:range withString:string]; //do some work returnValue = [self isStringValid:tmp]; if([self.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:)]) { //allow the next delegate in the chain to provide additional input returnValue = returnValue && [self.delegate textField:textField shouldChangeCharactersInRange:range replacementString:string]; } return returnValue; } //Here's our dumbed down validation logic //allow the field to be empty //do not allow any whitespace characters //if the remainder is a number between 0 and INT32_MAX, then its valid -(BOOL)isStringValid:(NSString *)string { if([string length] == 0) { return YES; } NSRange range = [string rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]]; if (range.location != NSNotFound) { return NO; } NSScanner * scan = [NSScanner localizedScannerWithString:string]; NSInteger holder; BOOL result = [scan scanInteger:&holder]; return result && [scan isAtEnd] && holder >= 0 && holder < INT32_MAX; } @end |
This code should look pretty familiar. We implement textField:shouldChangeCharactersInRange:replacementString: in much the same way as if we had done so in the view controller. We have an additional delegate so that we can still allow the view controller to implement the text field delegate methods if needed.
So the next thing to do is create the UITextField subclass to initialize the QuantityDelegate. Remember the QuantityField will hold a strong reference to the QuantityDelegate.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
//header @interface QuantityField : UITextField @end //implementation #import "QuantityDelegate.h" @interface QuantityField() @property (nonatomic, strong) QuantityDelegate* internalDelegate; @end @implementation QuantityField //this initializer is used when the quantity field is in a xib/nib or storyboard -(instanceType)initWithCoder:(NSCoder*)aDecoder { self = [super initWithCoder:aDecoder]; if(self) { _internalDelegate = [[QuantityDelegate alloc] init]; //call super because we're going to override this method in self [super setDelegate:_internalDelegate]; } return self; } //this initializer is used when the quantity field is initialized in code -(instanceType)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if(self) { _internalDelegate = [[QuantityDelegate alloc] init]; //call super because we're going to override this method in self [super setDelegate:_internalDelegate]; } return self; } //allow another delegate to be connected to the delegate chain -(void)setDelegate:(id<UITextFieldDelegate>)delegate { self.internalDelegate.delegate = delegate; } @end |
As we can see, the majority of our code is the insanely long init methods. If you’re only ever going to put a QuantityField in a xib/nib or storyboard, you can leave out initWithFrame:. And if you’re only going to initialize in code, you can leave out initWithCoder:. I typically put them both in, just in case I want to do both, but at the same time YAGNI.
With all of that work done, now we can throw a text field into interface builder, and set its class to QuantityField. The view controller can still be the QuantityField’s delegate if desired, and it can rest easy knowing that the QuantityField validates input itself.
If we wanted to use any other validation logic, we would just create a subclass of BaseSurrogateDelegate with the required validation logic, and create a UITextField with a reference to that subclass.
Block
To create the desired system, you’ll need to throw this class in your project.
BlockDelegate.h BlockDelegate.m
The class is simple. It has properties for you to be able to provide a block implementation for any possible UITextFieldDelegate method. If you don’t provide a block, and the BlockDelegate’s delegate doesn’t implement the selector, the BlockDelegate claims not to implement the selector. When any selector is called, it will execute the provided block (if there is one), and have its delegate execute its implementation of the UITextFieldDelegate method.
Looking at the code, you will see a whole lot of boring, boiler plate code.
The steps to using this solution are
- Create a UITextField subclass
- Initialize the BlockDelegate
- Setup the blocks on the BlockDelegate
We can do all of that with a single code example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
//header @interface QuantityField : UITextField @end //implementation @interface QuantityField() @property (nonatomic, strong) XCBlockTextFieldSurrogate* blockDelegate; @end @implementation QuantityField -(instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if(self) { [self commonInit]; } return self; } -(instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if(self) { [self commonInit]; } return self; } -(void)commonInit { _blockDelegate = [[XCBlockTextFieldSurrogate alloc] init]; _blockDelegate.textFieldShouldChangeCharactersInRangeBlock = ^(UITextField* textField, NSRange range, NSString* string){ //I chose not to put the logic in this block to keep things looking pretty QuantityField* field = (QuantityField*)textField; return [field changeResultsInValidPositiveInt32ForRange:range replacementString:string]; }; [super setDelegate:_blockDelegate]; } //implement the UITextFieldDelegate method required -(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string { BOOL returnValue = YES; NSString* tmp = [self.text stringByReplacingCharactersInRange:range withString:string]; //do some work returnValue = [self isStringValid:tmp]; if([self.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:)]) { //allow the next delegate in the chain to provide additional input returnValue = returnValue && [self.delegate textField:textField shouldChangeCharactersInRange:range replacementString:string]; } return returnValue; } //Here's our dumbed down validation logic //allow the field to be empty //do not allow any whitespace characters //if the remainder is a number between 0 and INT32_MAX, then its valid -(BOOL)isStringValid:(NSString *)string { if([string length] == 0) { return YES; } NSRange range = [string rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]]; if (range.location != NSNotFound) { return NO; } NSScanner * scan = [NSScanner localizedScannerWithString:string]; NSInteger holder; BOOL result = [scan scanInteger:&holder]; return result && [scan isAtEnd] && holder >= 0 && holder < INT32_MAX; } //allow another delegate to be connected to the delegate chain -(void)setDelegate:(id<UITextFieldDelegate>)delegate { self.blockDelegate.delegate = delegate; } @end |
With all of that work done, now we can throw a text field into interface builder, and set its class to QuantityField. The view controller can still be the QuantityField’s delegate if desired, and it can rest easy knowing that the QuantityField validates input itself.
If we wanted to use any other validation logic, we would just create a different subclass of UITextView with different blocks provided to the BlockDelegate.
Don’t worry, this doesn’t create a retain cycle. The text field retains the delegate, but the delegate doesn’t retain the text field. This really just makes it so the delegate has the same lifecycle as the text field. ↩