iOS Pattern UITextField using Runtime and without subclassing — NerdzLab

NerdzLab
NerdzLab
Mar 13, 2018 · 10 min read

What is it all about? It’s not just to show how we can build pattern UITextField. The actual logic of patterning is not so huge and definitely do not need a separate article to be explained. The main idea is — to show how to correctly build libraries, that could be used without any pain and how we can add enormous functionalities to already existing system classes.

Before we begin, I want you to know that inside this article will be a lot of Runtime code and it’s cool features. It will make our approach elegant and easy to use.

Now that the foundation has been set, let’s start.

How many times, during a development process, you had a task to implement some pattern when a user enters a text? By pattern I mean, automatically update text inputs when the user reaches certain points during text writings. For example, if the user writes his Credit Card number, it would be much better to update it to something like this “1234 5678 9098 7654”. Or if it’s a phone number Textfield it can be like this “+1 (234) 567–8909”. If you take a closer look, you will notice that all these inputs have some patterns and the user should not input characters that will be present anyway. In the end, previous examples have such patterns:

Credit Card — #### #### #### ####

Phone — +1 (###) ###-####

Pattern example

#. These symbols mean that the user could type anything there. All other characters should be filled when the cursor reaches a certain point.

I was searching if something similar to this exists but I wasn’t lucky enough. I found a few simple UITextfields, but they were all built for specific requirements(e.g. only for phones or credit cards). I thought it’s not good to use two different Textfields for the same task. Even if we would put a blind eye to it and use them, all the libraries have one, really huge WEAK POINT — to use that Textfields you need to inherit them from a specific class. As a result, you cannot use other Textfield libraries that requires inheriting, so you need to choose only one of it.

Now you know what the plan is, lets the process begin.

Firstly, let’s decide what language we should use, Swift or Objective-C. For most fans of Swift, I should tell you that it’s the worst language for library writing.

Why? Because no one wants to reimplement his code after new Swift version arrives. Also, it’s much usual to use Objective-C classes in Swift project than Swift in Objective-C.

For Swift lowers I will say — no matter what language will be used for this article, only implementation of idea matters.

Beginning

To begin with, we need to create a code that will apply the pattern to the string and also remove the pattern from it.

@interface NSString(BBBPattern)   - (nonnull NSString *)BBB_textWithoutPatternt:(nonnull NSString     *)pattern inRange:(nullable NSRange *)range;   - (nonnull NSString *)BBB_textWithPatternt:(nonnull NSString *)pattern withRange:(nullable NSRange *)range;@end

We will create an extension to NSString class. Here, you can see two methods. The first one is that we remove applied pattern from a String. The second one is we apply the pattern to a String.

@implementation NSString(BBBPattern)   - (NSString *)BBB_textWithoutPatternt:(NSString *)pattern inRange:(NSRange *)range {
if (pattern.length == 0) {
return self;
}
NSMutableString *result = [NSMutableString new];
NSUInteger length = 0;
NSUInteger location = 0;
for (NSInteger i = 0; i < MIN(pattern.length, self.length); ++i) {
unichar patternCh = [pattern characterAtIndex:i];
unichar textCh = [self characterAtIndex:i];
if (patternCh == '#') {
[result appendString:[NSString stringWithCharacters:&textChlength:1]];
} else if (patternCh != textCh) {
[result appendString:[NSString stringWithCharacters:&textChlength:1]];
} else if (range != nil) {
if (i < range->location) {
++location;
} else if (i < range->location + range->length) {
++length;
}
}
}
if (range != nil) {
range->location -= location;
range->length -= length;
}
return result;
}
- (NSString *)BBB_textWithPatternt:(NSString *)pattern withRange:(NSRange *)range {
NSMutableString *result = [NSMutableString new];
NSUInteger length = 0;
NSUInteger location = 0;
for (NSInteger i = 0, j = 0; i < pattern.length && j < self.length; ++i) {
unichar patternCh = [pattern characterAtIndex:i];
unichar textCh = [self characterAtIndex:j];
if (patternCh == '#') {
[result appendString:[NSString stringWithCharacters:&textChlength:1]];
++j;
} else {
[result appendString:[NSString stringWithCharacters:&patternChlength:1]];
if (range != nil && j < range->location) {
++location;
}
}
}
if (range != nil) {
range->length += length;
range->location += location;
}
return result;
}
@end

With the above, you can check its implementation. Both methods receive two parameters. The first one is a pattern which we will work with. The second one is a range that after the algorithm will represent the same character range as before its work(it may differ from the original one). Its location is increased/decreased if the element was added/removed before passed range (that means a whole range moved by one). The Same length is increased/decreased if characters added/removed in between the beginning and end of the range(that means beginning will not move, but the end will). If the character was added/removed after the end of the passed range then nothing will happen as characters in passed range will not move.

Now, we need to use this method inside our UITextField extension. Logically thinking we should change our text, or in other words apply pattern changes, when Textfield text changes. There could be two situations when it changes:
1. Using text property from code
2. When user types a text in it

The first situation will be handled by the swizzling setText method.

The second one is more interesting. We’re getting notified whenever the user changes the text inside UITextFieldDelegate methods.

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

To receive whenever it’s called, we need to become a delegate to ourselves. It can be handled by the swizzling setDelegate method, but there is one tiny issue here. If a Textfield has a real delegate then we cannot change it so easily to ourselves. To make it work, we need to become a proxy class for UITextFieldDelegate calls. This means that we will be a delegate, but all delegate calls will be redirected to a real one. No one will even notice that they’re all not real delegates. No more words, let’s see the real code.

@interface UITextField(Exchange)   - (void)BBB_setText:(NSString *)text;   - (void)BBB_setDelegate:(id<UITextFieldDelegate>)delegate;   - (id<UITextFieldDelegate>)BBB_delegate;   - (id)BBB_forwardingTargetForSelector:(SEL)aSelector;   - (BOOL)BBB_respondsToSelector:(SEL)aSelector;@end
@interface UITextField(BBBDelegate) <UITextFieldDelegate>@end
@interface UITextField(BBBPrivate) + (void)BBB_adaptTextField:(UITextField *)textFieldtoExpression:(NSString *)expression; + (void)BBB_calculateText:(NSString *)textforTextField:(UITextField *)textFieldchangeBlock:(BBB_PatternTextFieldChangedBlock)blockwithRange:(NSRange)range; - (BOOL)BBB_isValidForText:(NSString*)text; - (void)BBB_registerListener; + (Protocol *)BBB_textFieldProtocol; - (BOOL)BBB_delegatesSelector:(SEL)aSelector;@end

Inside our UITextField extension we will have next extensions:
1. Exchange — represents original methods that will be exchanged with our own implementations.
2. BBBDelegate — conforming to UITextFieldDelegate
3. BBBPrivate — Private methods that will help us achieve the final result.
4. BBBPattern — you already saw that. It’s our public properties.

Let’s start with methods that will be exchanged.

@interface BBB_WeakObjectContainer : NSObject   @property (nonatomic, readonly, weak) id object;@end@implementation BBB_WeakObjectContainer   - (instancetype) initWithObject:(id)object   {      if (!(self = [super init])) {      return nil;   }   _object = object;   return self;}@end@implementation UITextField(Exchange)   - (void)BBB_setText:(NSString *)text {      if (![self BBB_isValidForText:text]) {         return;      }      [UITextField BBB_calculateText:text forTextField:self changeBlock:nil withRange:NSMakeRange(-1, -1)];   }   - (void)BBB_setDelegate:(id<UITextFieldDelegate>)delegate {      [self BBB_setDelegate: self];      BBB_WeakObjectContainer *object = [[BBB_WeakObjectContainer alloc] initWithObject:delegate];      objc_setAssociatedObject(self, @selector(BBB_setDelegate:), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);   }   - (id<UITextFieldDelegate>)BBB_delegate {      return ((BBB_WeakObjectContainer *)objc_getAssociatedObject(self, @selector(BBB_setDelegate:))).object;   }   - (id)BBB_forwardingTargetForSelector:(SEL)aSelector {      if ([self BBB_delegatesSelector:aSelector]) {         return self.delegate;      }      return [self BBB_forwardingTargetForSelector:aSelector];   }   - (BOOL)BBB_respondsToSelector:(SEL)aSelector {      if ([self BBB_delegatesSelector:aSelector]) {         return YES;      }      return [self BBB_respondsToSelector:aSelector];   }#pragma mark - Override   + (void)initialize {      static dispatch_once_t onceToken;      dispatch_once(&onceToken, ^{ method_exchangeImplementations(class_getInstanceMethod(self, @selector(setText:)),      class_getInstanceMethod(self, @selector(BBB_setText:)));      method_exchangeImplementations(class_getInstanceMethod(self, @selector(setDelegate:)),      class_getInstanceMethod(self, @selector(BBB_setDelegate:)));      method_exchangeImplementations(class_getInstanceMethod(self, @selector(delegate)),      class_getInstanceMethod(self, @selector(BBB_delegate)));      method_exchangeImplementations(class_getInstanceMethod(self, @selector(respondsToSelector:)),      class_getInstanceMethod(self, @selector(BBB_respondsToSelector:)));      method_exchangeImplementations(class_getInstanceMethod(self, @selector(forwardingTargetForSelector:)),      class_getInstanceMethod(self, @selector(BBB_forwardingTargetForSelector:)));      });   }@endBOOL BBB_selector_belongsToProtocol(SEL selector, Protocol * protocol) {   for (int optionbits = 0; optionbits < (1 << 2); optionbits++) {      BOOL required = optionbits & 1;      BOOL instance = !(optionbits & (1 << 1));      struct objc_method_description hasMethod = protocol_getMethodDescription(protocol, selector, required, instance);      if (hasMethod.name || hasMethod.types) {         return YES;      }   }   return NO;}

We need to reimplement next methods:
1. setText — to apply pattern if text was set from code
2. setDelegate — whenever anyone tries to set a delegate, we will only simulate setting, but delegate will not be set
3. delegate — as was told before delegate is fake, we need to return real delegate
4. forwardingTargetForSelector — same as we’re real delegates all methods from UITextFieldDelegate will be sent to us. We need to resend them to real delegate that was set by our user.
5. respondsToSelector — to let UITextFieldDelegate know that we will respond to any his selector that real delegate implements. We need that to redirect UITextFieldDelegate methods.
6. initialize — exchanging methods that were described before

To accomplish that, we need two more things.

The first one is a function that will let us know if the selector is from a specific protocol — BBB_selector_belongsToProtocol. It’s required to decide if we should try redirecting that method or not.
The second one is a class that contains weak property.

@interface BBB_WeakObjectContainer : NSObject   @property (nonatomic, readonly, weak) id object;@end@implementation BBB_WeakObjectContainer   - (instancetype) initWithObject:(id)object   {      if (!(self = [super init])) {         return nil;      }      _object = object;   return self;   }@end

Probably, you’ve noticed that a lot of properties are saved as associated objects. We were forced to do so because categories(extensions) cannot have stored properties, only calculated methods. As delegate should be saved as a weak reference to avoid reference cycle, we cannot directly make it weak using associated objects, there was a decision made to wrap into the weak property of another object. Using such approach will omit memory leak.

Next part would be private methods that were used for pattern updatings.

@implementation UITextField(BBBPrivate)   + (void)BBB_adaptTextField:(UITextField *)textField toExpression:(NSString *)expression {      NSString *nonPattern;      if (textField.BBB_pattern != nil) {         nonPattern = [textField.text BBB_textWithoutPatternt:textField.BBB_pattern inRange:nil];      } else {         nonPattern = textField.text;      }      NSString *text = [nonPattern BBB_normalizedTextWithExpression:expression];      if (textField.BBB_pattern != nil) {         text = [text BBB_textWithPatternt:textField.BBB_pattern withRange:nil];      }      if (![textField.text isEqualToString:text]) {         [textField BBB_setText: text];         if (textField.BBB_changedBlock != nil) {            textField.BBB_changedBlock(textField);         }      }   }   - (void)BBB_changeCursorForRange:(NSRange)range {      UITextPosition *beginning       = self.beginningOfDocument;      UITextPosition *cursorLocation  = [self positionFromPosition:beginning offset:(range.location + range.length)];      if(cursorLocation) {         [self setSelectedTextRange:[self textRangeFromPosition:cursorLocation toPosition:cursorLocation]];      }   }   + (void)BBB_calculateText:(NSString *)text forTextField:(UITextField *)textField changeBlock:(BBB_PatternTextFieldChangedBlock)block withRange:(NSRange)range {      if (textField.BBB_pattern.length != 0) {      NSString *pattern = [text BBB_textWithPatternt:textField.BBB_pattern withRange:&range];         if (![textField.text isEqualToString: pattern]) {            [textField BBB_setText: pattern];            [textField BBB_changeCursorForRange:range];            if (block != nil) {               block(textField);            }         }      } else {         [textField BBB_setText: text];         if (block != nil) {            block(textField);         }      }   }   - (BOOL)BBB_isValidForText:(NSString*)text {      if (self.BBB_regular.length != 0) {         NSPredicate *predicate = [NSPredicate predicateWithFormat: @"SELF MATCHES %@", self.BBB_regular];         return [predicate evaluateWithObject:text];      }      return YES;   }#pragma mark - Protocols   + (Protocol *)BBB_textFieldProtocol {      return objc_getProtocol([@"UITextFieldDelegate" cStringUsingEncoding:[NSString defaultCStringEncoding]]);   }   - (BOOL)BBB_delegatesSelector:(SEL)aSelector {      if (self.delegate != nil) {         return [self.delegate respondsToSelector:aSelector] && BBB_selector_belongsToProtocol(aSelector, [UITextField BBB_textFieldProtocol]);      }      return NO;   }@end

Let’s describe one by one each method, so it would be more clear for you where are we moving.

1. + (void)BBB_adaptTextField:(UITextField *)textField toExpression:(NSString *)expression — will adapt Textfield text to regular expression. To make it even easier to work with, I’ve added one more feature to it. It’s conformance to specific regular expression, so the user cannot type symbols that will make our text incorrect. This method will begin its work if a new regular expression is set. It will adapt currently existing text to new expression by removing unmatched symbols.

2. - (void)BBB_changeCursorForRange:(NSRange)range — after our manipulations with adding/removing characters, cursor also needs to be in the correct place. This method will move our cursor to the right place.

3. + (void)BBB_calculateText:(NSString *)text forTextField:(UITextField *)textField changeBlock:(BBB_PatternTextFieldChangedBlock)block withRange:(NSRange)range — this is a heart of the idea. It’s used anytime text is changed to calculate it’s real value due to current pattern. Parameter NSRange was described before.

4. - (BOOL)BBB_isValidForText:(NSString*)text — checks if a passed text is good for current regular expression that is assigned to our Textfield.

5. + (Protocol *)BBB_textFieldProtocol — this method returns special metadata from UITextFieldDelegate protocol. We need that for a previously described function which will then return if selector exists in a specific protocol or not.

6. - (BOOL)BBB_delegatesSelector:(SEL)aSelector — this method is used to check if we should delegate a method or not, which is based on the selector and current Textfield delegate.

A little while and we will end our long journey with a great final result. Believe me, it’s worth it.

The next part is UITextFieldDelegate code. This has only one method.

@implementation UITextField(BBBDelegate)   - (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string {      NSString *text = [textField.text BBB_textWithoutPatternt:self.BBB_pattern inRange:&range];      NSString *final = [text stringByReplacingCharactersInRange:range withString:string];      if (![textField BBB_isValidForText:final]) {         return NO;      }      if (self.BBB_pattern.length != 0) {         [textField BBB_setText:text];         if (![self.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:)] || [self.delegate textField:textFiel shouldChangeCharactersInRange:range replacementString:string]) {            NSRange positionRange = NSMakeRange(range.location + string.length, 0);            [UITextField BBB_calculateText:final forTextField:textField changeBlock:textField.BBB_changedBlock withRange:positionRange];         }         return NO;      }      if ([self.delegate respondsToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString:)]) {         return [self.delegate textField:textField shouldChangeCharactersInRange:range replacementString:string];      }      return YES;   }@end

The idea here is to change text due to pattern and redirect this method to the real delegate. So no one will even notice we were doing something. For our users, it’s the same UITextField with all its capabilities.

The last part is actually Public API that all users will see. As I mentioned before, we will use IBInspectable properties to give user possibility to configure it from UI files.

And here it is. Tadaaaaa
Public API:

typedef void (^BBB_PatternTextFieldChangedBlock)(UITextField * _Nonnull);@interface UITextField(BBBPattern)   @property (nonatomic, strong, nullable) IBInspectable NSString *BBB_pattern;   @property (nonatomic, strong, nullable) IBInspectable NSString *BBB_regular;   @property (nonatomic, strong, nullable, readonly) NSString *BBB_nonPatternText;   @property (nonatomic, copy, nullable) BBB_PatternTextFieldChangedBlock BBB_changedBlock;@end

Private implementation of public API.

@implementation UITextField(BBBPattern)   @dynamic BBB_pattern;   @dynamic BBB_regular;   - (NSString *)BBB_pattern {      return objc_getAssociatedObject(self, @selector(BBB_pattern));   }   - (void)setBBB_pattern:(NSString *)BBB_pattern {      if (![BBB_pattern isEqualToString:self.BBB_pattern]) {         NSString *text = [self.text BBB_textWithoutPatternt:self.BBB_pattern inRange:nil];         objc_setAssociatedObject(self, @selector(BBB_pattern), BBB_pattern, OBJC_ASSOCIATION_RETAIN_NONATOMIC);         [UITextField BBB_calculateText:text forTextField:self changeBlock:nil withRange:NSMakeRange(-1, -1)];         [self BBB_registerListener];      }   }   - (NSString *)BBB_regular {      return objc_getAssociatedObject(self, @selector(BBB_regular));   }   - (void)setBBB_regular:(NSString *)BBB_regular {      if (![BBB_regular isEqualToString:self.BBB_regular]) {         objc_setAssociatedObject(self, @selector(BBB_regular), BBB_regular, OBJC_ASSOCIATION_RETAIN_NONATOMIC);         [UITextField BBB_adaptTextField:self toExpression:BBB_regular];         [self BBB_registerListener];      }   }  - (BBB_PatternTextFieldChangedBlock)BBB_changedBlock {      return objc_getAssociatedObject(self, @selector(BBB_changedBlock));  }   - (void)setBBB_changedBlock:(BBB_PatternTextFieldChangedBlock)BBB_changedBlock {      objc_setAssociatedObject(self, @selector(BBB_changedBlock), BBB_changedBlock, OBJC_ASSOCIATION_RETAIN_NONATOMIC);      [self BBB_registerListener];   }   - (NSString *)BBB_nonPatternText {      if (self.BBB_pattern != nil) {      return self.text == nil ? nil : [self.text BBB_textWithoutPatternt:self.BBB_pattern inRange:nil];      }      return self.text;   }@end

This last part contains saving all public data into associated objects(as extension can not have ivar variables). Sometimes if saved data can make text invalid, we’re recalculating dependent parts, just to keep everything up to date. For example if pattern changes, we need change current text if it does not meet with setted value.
Only one new feature was added here, and it’s callback(BBB_changeBlock) that our user can set to get notified when text changed. As usual here, we’re saving everything using associated objects.

It’s now the end, hope you liked how it works and expands your knowledge in iOS runtime. As Spiderman’s uncle said — “With great power comes great responsibility”. Use runtime wisely, do not let it mess up your code.

The full version of the code, you can find it in my repository.

NerdzLab

Written by

NerdzLab

For ideas never seen before. https://casestudies.nerdzlab.com/

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade