这是什么一回事呢? 这不仅仅是为了展示我们如何构建模式UITextField。 图案化的实际逻辑不是那么庞大,并且绝对不需要单独的文章进行解释。 主要思想是-展示如何正确构建可以轻松使用的库,以及如何为现有的系统类添加巨大的功能。
在开始之前,我希望您知道本文中将包含许多运行时代码,并且这些功能很酷。 这将使我们的方法优雅且易于使用。
现在已经建立了基础,让我们开始。
在开发过程中,您有多少次任务要在用户输入文本时实现某种模式? 我的意思是模式,当用户在文本编写过程中达到特定点时,将自动更新文本输入。 例如,如果用户写了他的信用卡号,最好将其更新为“ 1234 5678 9098 7654”。 或者,如果它是电话号码文本字段,则可以是“ +1(234)567-8909”。 如果仔细看,您会发现所有这些输入都有某些模式,并且用户无论如何都不应该输入将出现的字符。 最后,前面的示例具有以下模式:
- 预定和烘焙销售与跳蚤市场6月17日
- 人类图书馆打开了读者的视线
- 公共图书馆对当地社区至关重要,那么为什么我们要继续削减其资金呢?
- 图书馆员! 这是一个小蛇来照亮你的一天。
- 试试Golang! 电子表格のカラム文字列を数値に変换するライブラリ
信用卡 – #### #### #### ####
电话— +1( ### ) ### – ####


# 这些符号表示用户可以在此处键入任何内容。 当光标到达某个点时,所有其他字符都应填充。
我正在搜索是否存在类似的东西,但我还不够幸运。 我找到了一些简单的UITextfields,但它们都是针对特定要求而构建的(例如,仅用于电话或信用卡)。 我认为对同一任务使用两个不同的Textfields不好。 即使我们对此视而不见并使用它们,所有库都具有一个非常大的WEAK POINT-要使用该Textfield,您需要从特定类继承它们。 结果,您不能使用其他需要继承的Textfield库,因此您只需选择其中一个即可。
现在您知道了计划是什么,让过程开始。
首先,让我们决定应该使用哪种语言, Swift或Objective-C 。 对于大多数Swift爱好者来说,我应该告诉你这是最糟糕的库编写语言。
为什么? 因为没有人想在新的Swift版本到来后重新实现他的代码。 另外,在Swift项目中使用Objective-C类比在Objective-C中使用Swift更为常见。
对于Swift的下位人士,我会说-无论本文使用哪种语言,只有思想的实现才重要。
开始
首先,我们需要创建一个将模式应用于字符串并从中删除模式的代码。
@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
我们将创建NSString类的扩展 。 在这里,您可以看到两种方法。 第一个是我们从字符串中删除应用的模式。 第二个是我们将模式应用于字符串。
@implementation NSString(BBBPattern)-(NSString *)BBB_textWithoutPatternt:(NSString *)pattern inRange:(NSRange *)range {
如果(pattern.length == 0){
返回自我
} NSMutableString * result = [NSMutableString new];
NSUInteger长度= 0;
NSUInteger位置= 0; 对于(NSInteger i = 0; i <MIN(pattern.length,self.length); ++ i){
unichar patternCh = [pattern characterAtIndex:i];
unichar textCh = [self characterAtIndex:i];
如果(patternCh =='#'){
[结果appendString:[NSString stringWithCharacters:&textChlength:1]];
} else if(patternCh!= textCh){
[结果appendString:[NSString stringWithCharacters:&textChlength:1]];
} else if(range!= nil){
如果(i location){
++位置;
} else if(i 位置+范围->长度){
++长度;
}
}
}
如果(范围!=无){
范围->位置-=位置;
范围->长度-=长度;
}
返回结果;
}-(NSString *)BBB_textWithPatternt:(NSString *)pattern withRange:(NSRange *)range {
NSMutableString * result = [NSMutableString new];
NSUInteger长度= 0;
NSUInteger位置= 0; 对于(NSInteger i = 0,j = 0; i <pattern.length && j <self.length; ++ i){
unichar patternCh = [pattern characterAtIndex:i];
unichar textCh = [self characterAtIndex:j];
如果(patternCh =='#'){
[结果appendString:[NSString stringWithCharacters:&textChlength:1]];
++ j;
}其他{
[结果appendString:[NSString stringWithCharacters:&patternChlength:1]];
if(range!= nil && j location){
++位置;
}
}
}
如果(范围!=无){
范围->长度+ =长度;
范围->位置+ =位置;
}
返回结果;
}
@结束
通过上面的内容,您可以检查其实现。 两种方法都接收两个参数。 第一个是我们将使用的模式。 第二个范围是一个范围,在该范围之后,算法将表示与工作之前相同的字符范围(可能与原始范围不同)。 如果在通过范围之前添加/删除了元素(这意味着整个范围移动了一个),则其位置将增加/减少。 如果在范围的开头和结尾之间添加/删除了字符,则增加或减少相同长度(这意味着开头将不会移动,但结尾会移动)。 如果在通过范围的末端之后添加/删除了字符,则不会发生任何事情,因为通过范围内的字符将不会移动。
现在,我们需要在UITextField扩展内使用此方法。 逻辑上认为,当Textfield文本更改时,我们应该更改文本,或者换句话说,应用模式更改。 更改时可能有两种情况:
1. 使用代码中的text属性
2. 当用户在其中键入文本时
第一种情况将由麻烦的setText方法处理。
第二个更有趣。 每当用户更改UITextFieldDelegate方法中的文本时,我们都会收到通知。
-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)范围替换字符串:(NSString *)字符串
要在调用时接收,我们需要成为自己的代表。 可以通过麻烦的setDelegate方法处理它,但是这里有一个小问题。 如果一个Textfield有一个真正的委托,那么我们就不能轻易地将其更改为我们自己。 为了使其工作,我们需要成为UITextFieldDelegate调用的代理类 。 这意味着我们将成为一个委托,但是所有委托调用将被重定向到一个真实的委托。 甚至没有人会注意到他们都不是真正的代表。 不用多说了,让我们看看真正的代码。
@interface UITextField(Exchange)-(void)BBB_setText:(NSString *)text; -(void)BBB_setDelegate:(id )delegate; -(id )BBB_delegate; -(id)BBB_forwardingTargetForSelector:(SEL)aSelector; -(BOOL)BBB_respondsToSelector:(SEL)aSelector; @end
@interface UITextField(BBBDelegate) @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; +(协议*)BBB_textFieldProtocol; -(BOOL)BBB_delegatesSelector:(SEL)aSelector; @end
在我们的UITextField扩展内,我们将有下一个扩展:
1. 交换 -表示将与我们自己的实现交换的原始方法。
2. BBBDelegate —符合UITextFieldDelegate
3. BBBPrivate —私有方法将帮助我们获得最终结果。
4. BBBPattern-您已经看到了。 这是我们的公共财产。
让我们从将要交换的方法开始。
@interface BBB_WeakObjectContainer:NSObject @property(非原子,只读,弱)id对象; @ end @ implementation BBB_WeakObjectContainer-(实例类型)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 )delegate {[self BBB_setDelegate:self]; BBB_WeakObjectContainer * object = [[BBB_WeakObjectContainer alloc] initWithObject:delegate]; objc_setAssociatedObject(自身,@selector(BBB_setDelegate :),对象,OBJC_ASSOCIATION_RETAIN_NONATOMIC); }-(id )BBB_delegate {return(((BBB_WeakObjectContainer *)objc_getAssociatedObject(self,@selector(BBB_setDelegate :)))。object; }-(id)BBB_forwardingTargetForSelector:(SEL)aSelector {if([[self BBB_delegatesSelector:aSelector]){返回self.delegate; }返回[self BBB_forwardingTargetForSelector:aSelector]; }-(BOOL)BBB_respondsToSelector:(SEL)aSelector {if([[self BBB_delegatesSelector:aSelector]){返回是; }返回[self BBB_respondsToSelector:aSelector]; } #pragma mark-覆盖+(void)初始化{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 :)),self_class_getInstanceMethod ,@selector(BBB_setDelegate :))); method_exchangeImplementations(class_getInstanceMethod(self,@selector(delegate)),class_getInstanceMethod(self,@selector(BBB_delegate)))); method_exchangeImplementations(class_getInstanceMethod(self,@selector(responds_SelectInstanceMethod,),) (self,@selector(BBB_respondsToSelector :))); method_exchangeImplementations(class_getInstanceMethod(self,@selector(forwardingTargetForSelector :)),class_getInstanceMethod(self,@selector(BBB_forwardingTargetForSelector :)));}); } @endBOOL BBB_selector_belongsToProtocol(SEL选择器,协议*协议){for(int optionbits = 0; optionbits <(1 << 2); optionbits ++){必选BOOL = optionbits&1; BOOL实例=!(optionbits&(1 << 1)); struct objc_method_description hasMethod = protocol_getMethodDescription(协议,选择器,必需,实例); 如果(hasMethod.name || hasMethod.types){返回YES; }返回否;}
我们需要重新实现以下方法:
1. setText —如果通过代码设置了文本,则应用模式
2. setDelegate-每当有人尝试设置委托时,我们只会模拟设置,而不会设置委托
3. 委托人 -如委托人伪造之前所告知,我们需要返回真实的委托人
4. forwardingTargetForSelector —与我们真正的委托相同,UITextFieldDelegate的所有方法都将发送给我们。 我们需要将它们重新发送给用户设置的真实委托。
5. responsesToSelector —让UITextFieldDelegate知道我们将响应真实委托实现的任何选择器。 我们需要重定向UITextFieldDelegate方法。
6. 初始化 -之前描述的交换方法
为此,我们还需要两件事。
第一个函数可以让我们知道选择器是否来自特定协议— BBB_selector_belongsToProtocol 。 需要决定是否应该尝试重定向该方法。
第二类是包含弱属性的类。
@interface BBB_WeakObjectContainer:NSObject @property(非原子,只读,弱)id对象; @ end @ implementation BBB_WeakObjectContainer-(实例类型)initWithObject:(id)object {if(!(self = [super init]))){return nil; } _object = object; 返回自我 }@结束
可能您已经注意到,很多属性都保存为关联对象 。 我们被迫这样做是因为类别(扩展名)不能具有存储的属性,而只能具有计算的方法。 由于应将委托另存为弱引用以避免引用循环,因此我们不能使用关联的对象直接使其变为弱引用,因此已决定将其包装为另一个对象的弱属性。 使用这种方法将忽略内存泄漏。
下一部分将是用于模式更新的私有方法。
@implementation UITextField(BBBPrivate)+(void)BBB_adaptTextField:(UITextField *)textField toExpression:(NSString *)expression {NSString * nonPattern; 如果(textField.BBB_pattern!= nil){nonPattern = [textField.text BBB_textWithoutPatternt:textField.BBB_pattern inRange:nil]; } else {nonPattern = textField.text; } NSString * text = [nonPattern BBB_normalizedTextWithExpression:expression]; 如果(textField.BBB_pattern!= nil){text = [text BBB_textWithPatternt:textField.BBB_pattern withRange:nil]; } if(![textField.text isEqualToString:text]){[textField BBB_setText:text]; 如果(textField.BBB_changedBlock!= nil){textField.BBB_changedBlock(textField); }}}-(void)BBB_changeCursorForRange:(NSRange)range {UITextPosition * beginning = self.beginningOfDocument; UITextPosition * cursorLocation = [self positionFromPosition:起始偏移量:(range.location + range.length)]; if(cursorLocation){[self setSelectedTextRange:[self textRangeFromPosition:cursorLocation to Position:cursorLocation]]; }} +(void)BBB_calculateText:(NSString *)text forTextField:(UITextField *)textField changeBlock:(BBB_PatternTextFieldChangedBlock)block withRange:(NSRange)range {if(textField.BBB_pattern.length!= 0){NSString * pattern = [文本BBB_textWithPatternt:textField.BBB_pattern withRange:&range]; 如果(![textField.text isEqualToString:pattern]){[textField BBB_setText:pattern]; [textField BBB_changeCursorForRange:range]; 如果(block!= nil){block(textField); }}} else {[textField BBB_setText:text]; 如果(block!= nil){block(textField); }}}-(BOOL)BBB_isValidForText:(NSString *)text {if(self.BBB_regular.length!= 0){NSPredicate * predicate = [NSPredicate predicateWithFormat:@“ SELF MATCHES%@”,self.BBB_regular]; 返回[谓词evaluateWithObject:text]; }返回是; } #pragma mark-协议+(Protocol *)BBB_textFieldProtocol {return objc_getProtocol([@“ UITextFieldDelegate” cStringUsingEncoding:[NSString defaultCStringEncoding]])); }-(BOOL)BBB_delegatesSelector:(SEL)aSelector {if(self.delegate!= nil){返回[self.delegate responsesToSelector:aSelector] && BBB_selector_belongsToProtocol(aSelector,[UITextField BBB_textFieldProtocol]);; }返回NO; }@结束
让我们对每种方法进行一个一个地描述,这样对您来说我们将要移动到哪里会更清楚。
1. +(void)BBB_adaptTextField:(UITextField *)textField toExpression:(NSString *)expression —将使Textfield文本适应正则表达式。 为了使使用起来更容易,我添加了一项功能。 它符合特定的正则表达式,因此用户无法键入会使我们的文本不正确的符号。 如果设置了新的正则表达式,则此方法将开始工作。 通过删除不匹配的符号,它将使当前现有的文本适应新的表达方式。
2. –(void)BBB_changeCursorForRange:(NSRange)范围 -在我们添加/删除字符的操作之后,光标也需要放在正确的位置。 此方法会将光标移到正确的位置。
3. +(void)BBB_calculateText:(NSString *)文本forTextField:(UITextField *)textField changeBlock:(BBB_PatternTextFieldChangedBlock)block withRange:(NSRange)range —这是这个主意。 由于当前模式,文本更改后可随时用于计算其实际值。 之前已经描述了参数NSRange。
4. –(BOOL)BBB_isValidForText:(NSString *)text-检查传递的文本是否适合分配给我们Textfield的当前正则表达式。
5. +(Protocol *)BBB_textFieldProtocol-此方法从UITextFieldDelegate协议返回特殊的元数据。 我们需要一个用于前面描述的函数的函数,如果选择器是否存在于特定协议中,该函数将返回。
6. –(BOOL)BBB_delegatesSelector:(SEL)aSelector —此方法用于检查是否应该委托一个基于选择器和当前Textfield委托的方法。
稍等片刻,我们将以伟大的最终结果结束漫长的旅程。 相信我,这是值得的。
下一部分是UITextFieldDelegate代码。 这只有一种方法。
@implementation UITextField(BBBDelegate)-(BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replaceString:(NSString *)string {NSString * text = [textField.text BBB_textWithoutPatternt:self.BBB_pattern inRange:&range]; NSString * final = [text stringByReplacingCharactersInRange:range withString:string]; 如果(![textField BBB_isValidForText:final]){返回NO; } if(self.BBB_pattern.length!= 0){[textField BBB_setText:text]; 如果(![self.delegate responsesToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString :)] || [self.delegate textField:textFiel shouldChangeCharactersInRange:range replaceString:string]){NSRange positionRange = NSMakeRange(range.location + string.length ,0); [UITextField BBB_calculateText:final forTextField:textField changeBlock:textField.BBB_changedBlock withRange:positionRange]; }返回NO; } if([[self.delegate responsesToSelector:@selector(textField:shouldChangeCharactersInRange:replacementString :)])} {返回[self.delegate textField:textField shouldChangeCharactersInRange:range replaceString:string]; }返回是; }@结束
这里的想法是根据模式更改文本,然后将此方法重定向到实际的委托 。 所以没有人会注意到我们在做某事。 对于我们的用户,它是具有所有功能的相同UITextField。
最后一部分实际上是所有用户都将看到的Public API。 如前所述,我们将使用IBInspectable属性使用户可以从UI文件进行配置。
在这里。 塔达阿阿
公开API:
typedef void(^ BBB_PatternTextFieldChangedBlock)(UITextField * _Nonnull); @ interface UITextField(BBBPattern)@属性(非原子,强,可空)IBInspectable NSString * BBB_pattern; @属性(非原子,强,可为空)IBInspectable NSString * BBB_regular; @属性(非原子,强,可为空,只读)NSString * BBB_nonPatternText; @属性(非原子,复制,可为空)BBB_PatternTextFieldChangedBlock BBB_changedBlock; @end
公共API的私有实现。
@implementation UITextField(BBBPattern)@动态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){返回self.text == nil吗? nil:[self.text BBB_textWithoutPatternt:self.BBB_pattern inRange:nil]; }返回self.text; }@结束
最后一部分包含将所有公共数据保存到关联的对象中(因为扩展名不能包含ivar变量)。 有时,如果保存的数据会使文本无效,我们将重新计算相关部分,以使所有内容保持最新。 例如,如果模式更改,则如果当前文本不符合设置值,则需要更改当前文本。
此处仅添加了一项新功能,它是我们的用户可以设置的回调( BBB_changeBlock ),以在文本更改时得到通知。 像往常一样,我们使用关联的对象保存所有内容。
现在到此结束,希望您喜欢它的工作原理并扩展您在iOS运行时中的知识。 正如蜘蛛侠的叔叔所说的那样:“强大的力量伴随着巨大的责任”。 明智地使用运行时,不要让它弄乱您的代码。
完整版本的代码,可以在我的存储库中找到。