YMCaptchaTextView.m 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. //
  2. // YMCaptchaTextView.m
  3. // MSYOUPAI
  4. //
  5. // Created by YoMi on 2023/5/5.
  6. //
  7. #import "YMCaptchaTextView.h"
  8. #import "YMCaptchaTextField.h"
  9. typedef NS_ENUM(NSInteger, YMCaptchaTextChangeType) {
  10. YMCaptchaTextChangeType_NoChange,
  11. YMCaptchaTextChangeType_Insert,
  12. YMCaptchaTextChangeType_Delete,
  13. };
  14. @interface YMCaptchaTextView () <UICollectionViewDataSource, UICollectionViewDelegate, UITextFieldDelegate>
  15. {
  16. NSInteger _oldLength;
  17. BOOL _ifNeedBeginEdit;
  18. }
  19. @property (nonatomic, assign) NSInteger codeLength;
  20. @property (nonatomic, strong) UITapGestureRecognizer *tap;
  21. @property (nonatomic, strong) YMCaptchaTextField *textField;
  22. @property (nonatomic, strong) UICollectionView *mainCollectionView;
  23. @property (nonatomic, strong) NSMutableArray <NSString *> *valueArr;
  24. @property (nonatomic, strong) NSMutableArray <YMCaptchaTextCellProperty *> *cellPropertyArr;
  25. @end
  26. @implementation YMCaptchaTextView
  27. - (instancetype)initWithFrame:(CGRect)frame{
  28. if (self = [super initWithFrame:frame]) {
  29. [self initDefaultValue];
  30. [self addNotificationObserver];
  31. }
  32. return self;
  33. }
  34. - (instancetype)initWithCoder:(NSCoder *)coder{
  35. if (self = [super initWithCoder:coder]) {
  36. [self initDefaultValue];
  37. [self addNotificationObserver];
  38. }
  39. return self;
  40. }
  41. - (instancetype)init{
  42. if (self = [super init]) {
  43. [self initDefaultValue];
  44. [self addNotificationObserver];
  45. }
  46. return self;
  47. }
  48. - (instancetype _Nullable )initWithCodeLength:(NSInteger)codeLength{
  49. if (self = [super init]) {
  50. [self initDefaultValue];
  51. [self addNotificationObserver];
  52. self.codeLength = codeLength;
  53. }
  54. return self;
  55. }
  56. - (void)dealloc{
  57. [self removeNotificationObserver];
  58. }
  59. #pragma mark - Notification Observer
  60. - (void)addNotificationObserver{
  61. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil];
  62. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil];
  63. }
  64. - (void)removeNotificationObserver {
  65. [[NSNotificationCenter defaultCenter] removeObserver:self];
  66. }
  67. - (void)applicationWillResignActive:(NSNotification *)notification{
  68. // 触发home按下,光标动画移除
  69. }
  70. - (void)applicationDidBecomeActive:(NSNotification *)notification{
  71. // 重新进来后响应,光标动画重新开始
  72. [self reloadAllCell];
  73. }
  74. #pragma mark - You can inherit
  75. - (void)initDefaultValue{
  76. _oldLength = 0;
  77. self.ifNeedSecurity = NO;
  78. self.securityDelay = 0.3;
  79. self.codeLength = 4;
  80. self.ifNeedCursor = YES;
  81. self.keyBoardType = UIKeyboardTypeNumberPad;
  82. self.textType = YMCaptchaTextType_Number;
  83. self.customInputRegex = @"";
  84. self.backgroundColor = [UIColor clearColor];
  85. _valueArr = [NSMutableArray new];
  86. _ifNeedBeginEdit = NO;
  87. }
  88. #pragma mark - LoadAndPrepareView
  89. - (void)loadAndPrepareView{
  90. [self loadAndPrepareViewWithBeginEdit:YES];
  91. }
  92. - (void)loadAndPrepareViewWithBeginEdit:(BOOL)beginEdit{
  93. if (_codeLength<=0) {
  94. NSAssert(NO, @"请输入大于0的验证码位数");
  95. return;
  96. }
  97. [self generateCellPropertyArr];
  98. // mainCollectionView
  99. if (!self.mainCollectionView || ![self.subviews containsObject:self.mainCollectionView]) {
  100. [self addSubview:self.mainCollectionView];
  101. [self.mainCollectionView mas_makeConstraints:^(MASConstraintMaker *make) {
  102. make.edges.mas_equalTo(UIEdgeInsetsZero);
  103. }];
  104. }
  105. // textField
  106. if (!self.textField || ![self.subviews containsObject:self.textField]) {
  107. [self addSubview:self.textField];
  108. [self.textField mas_makeConstraints:^(MASConstraintMaker *make) {
  109. make.width.height.mas_equalTo(0);
  110. make.left.top.mas_equalTo(0);
  111. }];
  112. }
  113. // tap
  114. if (self.tap.view != self) {
  115. [self addGestureRecognizer:self.tap];
  116. }
  117. if (![self.textField.text isEqualToString:self.customCellProperty.originValue]) {
  118. self.textField.text = self.customCellProperty.originValue;
  119. [self textDidChange:self.textField];
  120. }
  121. if (beginEdit) {
  122. [self beginEdit];
  123. }
  124. }
  125. - (void)generateCellPropertyArr{
  126. [self.cellPropertyArr removeAllObjects];
  127. for (int i = 0; i < self.codeLength; i++) {
  128. [self.cellPropertyArr addObject:[self.customCellProperty copy]];
  129. }
  130. }
  131. #pragma mark - code Length 调整
  132. - (void)resetCodeLength:(NSInteger)codeLength beginEdit:(BOOL)beginEdit{
  133. if (codeLength <= 0) {
  134. NSAssert(NO, @"请输入大于0的验证码位数");
  135. return;
  136. }
  137. self.codeLength = codeLength;
  138. [self generateCellPropertyArr];
  139. [self clearAllWithBeginEdit:beginEdit];
  140. }
  141. #pragma mark - Reload Input View
  142. - (void)reloadInputString:(NSString *_Nullable)value{
  143. if (![self.textField.text isEqualToString:value]) {
  144. self.textField.text = value;
  145. [self baseTextDidChange:self.textField manualInvoke:YES];
  146. }
  147. }
  148. #pragma mark - UITextFieldDelegate
  149. - (void)textFieldDidBeginEditing:(UITextField *)textField{
  150. _ifNeedBeginEdit = YES;
  151. if (self.ifClearAllInBeginEditing && self.textValue.length == self.codeLength) {
  152. [self clearAll];
  153. }
  154. if (self.textEditStatusChangeblock) {
  155. self.textEditStatusChangeblock(YMCaptchaTextEditStatus_BeginEdit);
  156. }
  157. [self reloadAllCell];
  158. }
  159. - (void)textFieldDidEndEditing:(UITextField *)textField{
  160. _ifNeedBeginEdit = NO;
  161. if (self.textEditStatusChangeblock) {
  162. self.textEditStatusChangeblock(YMCaptchaTextEditStatus_EndEdit);
  163. }
  164. [self reloadAllCell];
  165. }
  166. #pragma mark - TextViewEdit
  167. - (void)beginEdit{
  168. if (![self.textField isFirstResponder]) {
  169. [self.textField becomeFirstResponder];
  170. }
  171. }
  172. - (void)endEdit{
  173. if ([self.textField isFirstResponder]) {
  174. [self.textField resignFirstResponder];
  175. }
  176. }
  177. - (void)clearAll{
  178. [self clearAllWithBeginEdit:YES];
  179. }
  180. - (void)clearAllWithBeginEdit:(BOOL)beginEdit{
  181. _oldLength = 0;
  182. [_valueArr removeAllObjects];
  183. self.textField.text = @"";
  184. [self allSecurityClose];
  185. [self reloadAllCell];
  186. [self triggerBlock];
  187. if (beginEdit) {
  188. [self beginEdit];
  189. }
  190. }
  191. #pragma mark - UITextFieldDidChange
  192. - (void)textDidChange:(UITextField *)textField {
  193. [self baseTextDidChange:textField manualInvoke:NO];
  194. }
  195. /**
  196. * 过滤输入内容
  197. */
  198. - (NSString *)filterInputContent:(NSString *)inputStr {
  199. NSMutableString *mutableStr = [[NSMutableString alloc] initWithString:inputStr];
  200. if (self.textType == YMCaptchaTextType_Number) {
  201. /// 纯数字
  202. NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"[^0-9]" options:0 error:nil];
  203. [regex replaceMatchesInString:mutableStr options:0 range:NSMakeRange(0, [mutableStr length]) withTemplate:@""];
  204. } else if (self.textType == YMCaptchaTextType_Normal) {
  205. /// 不处理
  206. nil;
  207. } else if (self.textType == YMCaptchaTextType_Regex) {
  208. /// 自定义正则
  209. if (self.customInputRegex.length > 0) {
  210. NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:self.customInputRegex options:0 error:nil];
  211. [regex replaceMatchesInString:mutableStr options:0 range:NSMakeRange(0, [mutableStr length]) withTemplate:@""];
  212. }
  213. }
  214. return [mutableStr copy];
  215. }
  216. /**
  217. * textDidChange基操作
  218. * manualInvoke:是否为手动调用
  219. */
  220. - (void)baseTextDidChange:(UITextField *)textField manualInvoke:(BOOL)manualInvoke {
  221. __weak typeof(self) weakSelf = self;
  222. NSString *verStr = textField.text;
  223. //有空格去掉空格
  224. verStr = [verStr stringByReplacingOccurrencesOfString:@" " withString:@""];
  225. verStr = [self filterInputContent:verStr];
  226. //自定义处理
  227. if (self.textCustomProcessblock) {
  228. verStr = self.textCustomProcessblock(verStr);
  229. }
  230. if (verStr.length >= _codeLength) {
  231. verStr = [verStr substringToIndex:_codeLength];
  232. [self endEdit];
  233. }
  234. textField.text = verStr;
  235. // 判断删除/增加
  236. YMCaptchaTextChangeType captchaTextChangeType = YMCaptchaTextChangeType_NoChange;
  237. if (verStr.length > _oldLength) {
  238. captchaTextChangeType = YMCaptchaTextChangeType_Insert;
  239. }else if (verStr.length < _oldLength){
  240. captchaTextChangeType = YMCaptchaTextChangeType_Delete;
  241. }
  242. // _valueArr
  243. if (captchaTextChangeType == YMCaptchaTextChangeType_Delete) {
  244. [self setSecurityShow:NO index:_valueArr.count-1];
  245. [_valueArr removeLastObject];
  246. }else if (captchaTextChangeType == YMCaptchaTextChangeType_Insert){
  247. if (verStr.length > 0) {
  248. if (_valueArr.count > 0) {
  249. [self replaceValueArrToAsteriskWithIndex:_valueArr.count - 1 needEqualToCount:NO];
  250. }
  251. // NSString *subStr = [verStr substringWithRange:NSMakeRange(verStr.length - 1, 1)];
  252. // [strongSelf.valueArr addObject:subStr];
  253. [_valueArr removeAllObjects];
  254. [verStr enumerateSubstringsInRange:NSMakeRange(0, verStr.length) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
  255. __strong __typeof(weakSelf)strongSelf = weakSelf;
  256. [strongSelf.valueArr addObject:substring];
  257. }];
  258. if (self.ifNeedSecurity) {
  259. if (manualInvoke) {
  260. // 处理所有秘文
  261. [self delaySecurityProcessAll];
  262. }else {
  263. // 只处理最后一个秘文
  264. [self delaySecurityProcessLastOne];
  265. }
  266. }
  267. }
  268. }
  269. [self reloadAllCell];
  270. _oldLength = verStr.length;
  271. if (captchaTextChangeType != YMCaptchaTextChangeType_NoChange) {
  272. [self triggerBlock];
  273. }
  274. }
  275. #pragma mark - Control security show
  276. - (void)setSecurityShow:(BOOL)isShow index:(NSInteger)index{
  277. if (index < 0) {
  278. NSAssert(NO, @"index必须大于等于0");
  279. return;
  280. }
  281. YMCaptchaTextCellProperty *cellProperty = self.cellPropertyArr[index];
  282. cellProperty.ifShowSecurity = isShow;
  283. }
  284. - (void)allSecurityClose{
  285. [self.cellPropertyArr enumerateObjectsUsingBlock:^(YMCaptchaTextCellProperty * _Nonnull cellProperty, NSUInteger idx, BOOL * _Nonnull stop) {
  286. if (cellProperty.ifShowSecurity == YES) {
  287. cellProperty.ifShowSecurity = NO;
  288. }
  289. }];
  290. }
  291. - (void)allSecurityOpen{
  292. [self.cellPropertyArr enumerateObjectsUsingBlock:^(YMCaptchaTextCellProperty * _Nonnull cellProperty, NSUInteger idx, BOOL * _Nonnull stop) {
  293. if (cellProperty.ifShowSecurity == NO) {
  294. cellProperty.ifShowSecurity = YES;
  295. }
  296. }];
  297. }
  298. #pragma mark - Trigger block
  299. - (void)triggerBlock{
  300. if (self.textDidChangeblock) {
  301. BOOL isFinished = _valueArr.count == _codeLength ? YES : NO;
  302. self.textDidChangeblock(_textField.text, isFinished);
  303. }
  304. }
  305. #pragma mark - Asterisk 替换密文
  306. /**
  307. * 替换密文
  308. * needEqualToCount:是否只替换最后一个
  309. */
  310. - (void)replaceValueArrToAsteriskWithIndex:(NSInteger)index needEqualToCount:(BOOL)needEqualToCount{
  311. if (!self.ifNeedSecurity) {
  312. return;
  313. }
  314. if (needEqualToCount && index != _valueArr.count - 1) {
  315. return;
  316. }
  317. [self setSecurityShow:YES index:index];
  318. }
  319. #pragma mark 延时替换最后一个密文
  320. - (void)delaySecurityProcessLastOne{
  321. __weak __typeof(self)weakSelf = self;
  322. [self delayAfter:self.securityDelay dealBlock:^{
  323. __strong __typeof(weakSelf)strongSelf = weakSelf;
  324. if (strongSelf.valueArr.count > 0) {
  325. [strongSelf replaceValueArrToAsteriskWithIndex:strongSelf.valueArr.count-1 needEqualToCount:YES];
  326. dispatch_async(dispatch_get_main_queue(), ^{
  327. [strongSelf reloadAllCell];
  328. });
  329. }
  330. }];
  331. }
  332. #pragma mark 延时替换所有一个密文
  333. - (void)delaySecurityProcessAll{
  334. __weak __typeof(self)weakSelf = self;
  335. [self.valueArr enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
  336. __strong __typeof(weakSelf)strongSelf = weakSelf;
  337. [strongSelf replaceValueArrToAsteriskWithIndex:idx needEqualToCount:NO];
  338. }];
  339. [self reloadAllCell];
  340. }
  341. #pragma mark - DelayBlock
  342. - (void)delayAfter:(CGFloat)delayTime dealBlock:(void (^)(void))dealBlock{
  343. dispatch_time_t timer = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayTime *NSEC_PER_SEC));
  344. dispatch_after(timer, dispatch_get_main_queue(), ^{
  345. if (dealBlock) {
  346. dealBlock();
  347. }
  348. });
  349. }
  350. #pragma mark - UICollectionViewDataSource
  351. - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section{
  352. return _codeLength;
  353. }
  354. - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath{
  355. id tempCell = [self customCollectionView:collectionView cellForItemAtIndexPath:indexPath];
  356. if ([tempCell isKindOfClass:[YMCaptchaTextCell class]]) {
  357. YMCaptchaTextCell *cell = (YMCaptchaTextCell *)tempCell;
  358. cell.ifNeedCursor = self.ifNeedCursor;
  359. // CellProperty
  360. YMCaptchaTextCellProperty *cellProperty = self.cellPropertyArr[indexPath.row];
  361. cellProperty.index = indexPath.row;
  362. NSString *currentPlaceholderStr = nil;
  363. if (_placeholderText.length > indexPath.row) {
  364. currentPlaceholderStr = [_placeholderText substringWithRange:NSMakeRange(indexPath.row, 1)];
  365. cellProperty.cellPlaceholderText = currentPlaceholderStr;
  366. }
  367. // setOriginValue
  368. NSUInteger focusIndex = _valueArr.count;
  369. if (_valueArr.count > 0 && indexPath.row <= focusIndex - 1) {
  370. [cellProperty setMyOriginValue:_valueArr[indexPath.row]];
  371. }else{
  372. [cellProperty setMyOriginValue:@""];
  373. }
  374. cell.captchaTextCellProperty = cellProperty;
  375. if (_ifNeedBeginEdit) {
  376. cell.selected = indexPath.row == focusIndex ? YES : NO;
  377. }else{
  378. cell.selected = NO;
  379. }
  380. }
  381. return tempCell;
  382. }
  383. - (void)reloadAllCell{
  384. [self.mainCollectionView reloadData];
  385. NSUInteger focusIndex = _valueArr.count;
  386. /// 最后一个
  387. if (focusIndex == self.codeLength) {
  388. [self.mainCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:focusIndex - 1 inSection:0] atScrollPosition:UICollectionViewScrollPositionRight animated:YES];
  389. } else {
  390. [self.mainCollectionView scrollToItemAtIndexPath:[NSIndexPath indexPathForRow:focusIndex inSection:0] atScrollPosition:UICollectionViewScrollPositionCenteredHorizontally animated:YES];
  391. }
  392. }
  393. #pragma mark - Qiuck set
  394. - (void)quickSetSecuritySymbol:(NSString *)securitySymbol{
  395. if (securitySymbol.length != 1) {
  396. securitySymbol = @"✱";
  397. }
  398. self.customCellProperty.securitySymbol = securitySymbol;
  399. }
  400. #pragma mark - You can rewrite
  401. - (UICollectionViewCell *)customCollectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
  402. {
  403. YMCaptchaTextCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:YMCaptchaTextCellID forIndexPath:indexPath];
  404. return cell;
  405. }
  406. #pragma mark - Setter & Getter
  407. - (UITapGestureRecognizer *)tap{
  408. if (!_tap) {
  409. _tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(beginEdit)];
  410. }
  411. return _tap;
  412. }
  413. - (YMCaptchaTextFlowLayout *)captchaTextFlowLayout{
  414. if (!_captchaTextFlowLayout) {
  415. _captchaTextFlowLayout = [YMCaptchaTextFlowLayout new];
  416. _captchaTextFlowLayout.itemSize = CGSizeMake(42, 47);
  417. }
  418. return _captchaTextFlowLayout;
  419. }
  420. - (UICollectionView *)mainCollectionView{
  421. if (!_mainCollectionView) {
  422. _mainCollectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:self.captchaTextFlowLayout];
  423. _mainCollectionView.showsHorizontalScrollIndicator = NO;
  424. _mainCollectionView.backgroundColor = [UIColor clearColor];
  425. _mainCollectionView.delegate = self;
  426. _mainCollectionView.dataSource = self;
  427. _mainCollectionView.layer.masksToBounds = YES;
  428. _mainCollectionView.clipsToBounds = YES;
  429. [_mainCollectionView registerClass:[YMCaptchaTextCell class] forCellWithReuseIdentifier:YMCaptchaTextCellID];
  430. }
  431. return _mainCollectionView;
  432. }
  433. - (void)setCodeLength:(NSInteger)codeLength{
  434. _codeLength = codeLength;
  435. self.captchaTextFlowLayout.itemNum = codeLength;
  436. }
  437. - (void)setKeyBoardType:(UIKeyboardType)keyBoardType{
  438. _keyBoardType = keyBoardType;
  439. self.textField.keyboardType = keyBoardType;
  440. }
  441. - (YMCaptchaTextField *)textField{
  442. if (!_textField) {
  443. _textField = [YMCaptchaTextField new];
  444. // _textView.alpha = 0.1;
  445. // _textView.tintColor = [UIColor clearColor];
  446. // _textView.backgroundColor = [UIColor clearColor];
  447. // _textView.textColor = [UIColor clearColor];
  448. _textField.delegate = self;
  449. [_textField addTarget:self action:@selector(textDidChange:) forControlEvents:UIControlEventEditingChanged];
  450. }
  451. return _textField;
  452. }
  453. - (void)setTextContentType:(UITextContentType)textContentType{
  454. _textContentType = textContentType;
  455. _textField.textContentType = textContentType;
  456. }
  457. - (YMCaptchaTextCellProperty *)customCellProperty{
  458. if (!_customCellProperty) {
  459. _customCellProperty = [YMCaptchaTextCellProperty new];
  460. }
  461. return _customCellProperty;
  462. }
  463. - (NSMutableArray<YMCaptchaTextCellProperty *> *)cellPropertyArr{
  464. if (!_cellPropertyArr) {
  465. _cellPropertyArr = [NSMutableArray new];
  466. }
  467. return _cellPropertyArr;
  468. }
  469. - (NSString *)textValue{
  470. return _textField.text;
  471. }
  472. @synthesize inputAccessoryView = _inputAccessoryView;
  473. - (void)setInputAccessoryView:(UIView *)inputAccessoryView{
  474. _inputAccessoryView = inputAccessoryView;
  475. self.textField.inputAccessoryView = _inputAccessoryView;
  476. }
  477. - (UIView *)inputAccessoryView{
  478. return _inputAccessoryView;
  479. }
  480. - (void)setIfNeedSecurity:(BOOL)ifNeedSecurity{
  481. _ifNeedSecurity = ifNeedSecurity;
  482. if (ifNeedSecurity == YES) {
  483. [self allSecurityOpen];
  484. }else{
  485. [self allSecurityClose];
  486. }
  487. dispatch_async(dispatch_get_main_queue(), ^{
  488. [self reloadAllCell];
  489. });
  490. }
  491. @end