OC中類目無法直接添加屬性,可以通過runtime實現在類目中添加屬性。 在學習的過程中,試著為UITextField添加了一個類目,實現了當TextField被鍵盤遮住時視圖上移的功能,順便也添加了點擊空白回收鍵盤功能。效果預覽使用時不需要一句代碼就可以實現上述功能[github鏈接](https ...
OC中類目無法直接添加屬性,可以通過runtime實現在類目中添加屬性。
在學習的過程中,試著為UITextField添加了一個類目,實現了當TextField被鍵盤遮住時視圖上移的功能,順便也添加了點擊空白回收鍵盤功能。
效果預覽
使用時不需要一句代碼就可以實現上述功能
[github鏈接](https://github.com/a1419430265/CHTTextFieldHealper)
.h文件
1 // 2 // UITextField+CHTPositionChange.h 3 // CHTTextFieldHealper 4 // 5 // Created by risenb_mac on 16/8/17. 6 // Copyright © 2016年 risenb_mac. All rights reserved. 7 // 8 9 #import <UIKit/UIKit.h> 10 11 @interface UITextField (CHTHealper) 12 13 /** 14 * 是否支持視圖上移 15 */ 16 @property (nonatomic, assign) BOOL canMove; 17 /** 18 * 點擊回收鍵盤、移動的視圖,預設是當前控制器的view 19 */ 20 @property (nonatomic, strong) UIView *moveView; 21 /** 22 * textfield底部距離鍵盤頂部的距離 23 */ 24 @property (nonatomic, assign) CGFloat heightToKeyboard; 25 26 @property (nonatomic, assign, readonly) CGFloat keyboardY; 27 @property (nonatomic, assign, readonly) CGFloat keyboardHeight; 28 @property (nonatomic, assign, readonly) CGFloat initialY; 29 @property (nonatomic, assign, readonly) CGFloat totalHeight; 30 @property (nonatomic, strong, readonly) UITapGestureRecognizer *tapGesture; 31 @property (nonatomic, assign, readonly) BOOL hasContentOffset; 32 33 @end
在.h文件中聲明屬性之後需要在.m中重寫setter,getter方法
首先定義全局key用作關聯唯一標識符
1 static char canMoveKey; 2 static char moveViewKey;
@implementation UITextField (CHTHealper) @dynamic canMove; @dynamic moveView;
具體實現
1 - (void)setCanMove:(BOOL)canMove { 2 // 參數意義:關聯對象 ,關聯標識符,關聯屬性值,關聯策略 3 objc_setAssociatedObject(self, &canMoveKey, @(canMove), OBJC_ASSOCIATION_RETAIN_NONATOMIC); 4 } 5 6 - (BOOL)canMove { 7 // 關聯屬性值為對象類型,需要轉換 8 return [objc_getAssociatedObject(self, &canMoveKey) boolValue]; 9 }
想要實現鍵盤遮住TextField後視圖上移,首先應確定TextField是否被鍵盤遮住,需要知道TextField在整個屏幕中的位置
// 此方法可以獲得TextField左上角在當前window中的坐標 [self convertPoint:self.bounds.origin toView:[UIApplication sharedApplication].keyWindow]
還需要知道鍵盤高度,這點需要接受系統通知,但是什麼時候接受通知、註銷通知?
我的思路是在TextField成為第一響應者的時候,為TextField添加通知,但是如果直接重寫becomeFirstResponder方法會覆蓋掉UITextField本身的方法,造成的最明顯的後果就是沒有游標了……為了避免這個問題,我用了runtime另外一個強大的功能,方法交換
為了保證方法交換隻進行一次,使用dispatch_once
為了保證方法交換儘早執行,寫在了load方法中
1 + (void)load { 2 static dispatch_once_t onceToken; 3 dispatch_once(&onceToken, ^{ 4 SEL systemSel = @selector(initWithFrame:); 5 SEL mySel = @selector(setupInitWithFrame:); 6 [self exchangeSystemSel:systemSel bySel:mySel]; 7 8 SEL systemSel2 = @selector(becomeFirstResponder); 9 SEL mySel2 = @selector(newBecomeFirstResponder); 10 [self exchangeSystemSel:systemSel2 bySel:mySel2]; 11 12 SEL systemSel3 = @selector(resignFirstResponder); 13 SEL mySel3 = @selector(newResignFirstResponder); 14 [self exchangeSystemSel:systemSel3 bySel:mySel3]; 15 16 SEL systemSel4 = @selector(initWithCoder:); 17 SEL mySel4 = @selector(setupInitWithCoder:); 18 [self exchangeSystemSel:systemSel4 bySel:mySel4]; 19 }); 20 [super load]; 21 }
具體交換步驟
1 // 交換方法 2 + (void)exchangeSystemSel:(SEL)systemSel bySel:(SEL)mySel { 3 Method systemMethod = class_getInstanceMethod([self class], systemSel); 4 Method myMethod = class_getInstanceMethod([self class], mySel); 5 //首先動態添加方法,實現是被交換的方法,返回值表示添加成功還是失敗 6 BOOL isAdd = class_addMethod(self, systemSel, method_getImplementation(myMethod), method_getTypeEncoding(myMethod)); 7 if (isAdd) { 8 //如果成功,說明類中不存在這個方法的實現 9 //將被交換方法的實現替換到這個並不存在的實現 10 class_replaceMethod(self, mySel, method_getImplementation(systemMethod), method_getTypeEncoding(systemMethod)); 11 }else{ 12 //否則,交換兩個方法的實現 13 method_exchangeImplementations(systemMethod, myMethod); 14 } 15 }
在上面我交換了四組方法,兩組init方法,是為了保證無論是代碼創建的還是xib拖得TextField都進行初始化
1 - (instancetype)setupInitWithCoder:(NSCoder *)aDecoder { 2 [self setup]; 3 return [self setupInitWithCoder:aDecoder]; 4 } 5 6 - (instancetype)setupInitWithFrame:(CGRect)frame { 7 [self setup]; 8 return [self setupInitWithFrame:frame]; 9 } 10 11 - (void)setup { 12 self.heightToKeyboard = 10; 13 self.canMove = YES; 14 self.keyboardY = 0; 15 self.totalHeight = 0; 16 self.tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapAction)]; 17 }
在TextField成為第一響應者時,為self添加通知接收,為moveView添加點擊事件(實現點擊空白回收鍵盤),註銷第一響應者時,註銷通知,移除點擊事件
1 - (BOOL)newBecomeFirstResponder { 2 // 如果沒有設置moveView 預設為當前控制器的view 3 if (self.moveView == nil) { 4 self.moveView = [self viewController].view; 5 } 6 // 保證moveView只有一個本TextField的點擊事件 7 if (![self.moveView.gestureRecognizers containsObject:self.tapGesture]) { 8 [self.moveView addGestureRecognizer:self.tapGesture]; 9 } 10 // 當重覆點擊當前TextField時(重覆成為第一響應者)或設置為不可移動 不再添加通知 11 if ([self isFirstResponder] || !self.canMove) { 12 return [self newBecomeFirstResponder]; 13 } 14 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(showAction:) name:UIKeyboardWillShowNotification object:nil]; 15 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(hideAction:) name:UIKeyboardWillHideNotification object:nil]; 16 return [self newBecomeFirstResponder]; 17 } 18 19 - (BOOL)newResignFirstResponder { 20 // 確保當前moveView有當前點擊事件,移除 21 if ([self.moveView.gestureRecognizers containsObject:self.tapGesture]) { 22 [self.moveView removeGestureRecognizer:self.tapGesture]; 23 } 24 if (!self.canMove) { 25 return [self newResignFirstResponder]; 26 } 27 BOOL result = [self newResignFirstResponder]; 28 [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil]; 29 [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil]; 30 // 當另外一個TextField成為第一響應者,當前TextField註銷第一響應者時不會回收鍵盤,手動調用moveView改變方法 31 [self hideKeyBoard:0]; 32 return result; 33 } 34 //獲取當前TextField所在controller 35 - (UIViewController *)viewController { 36 UIView *next = self; 37 while (1) { 38 UIResponder *nextResponder = [next nextResponder]; 39 if ([nextResponder isKindOfClass:[UIViewController class]]) { 40 return (UIViewController *)nextResponder; 41 } 42 next = next.superview; 43 } 44 return nil; 45 }
接收到彈出鍵盤後調用的方法
1 - (void)showAction:(NSNotification *)sender { 2 if (!self.canMove) { 3 return; 4 } 5 // 獲取鍵盤高度以及鍵盤的Y坐標 6 self.keyboardY = [sender.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].origin.y; 7 self.keyboardHeight = [sender.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue].size.height; 8 [self keyboardDidShow]; 9 } 10 11 - (void)hideAction:(NSNotification *)sender { 12 if (!self.canMove || self.keyboardY == 0) { 13 return; 14 } 15 [self hideKeyBoard:0.25]; 16 } 17 18 - (void)keyboardDidShow { 19 if (self.keyboardHeight == 0) { 20 return; 21 } 22 // 獲取TextField在window中的Y坐標 23 CGFloat fieldYInWindow = [self convertPoint:self.bounds.origin toView:[UIApplication sharedApplication].keyWindow].y; 24 // 確定是否需要視圖上移,以及移動的距離 25 CGFloat height = (fieldYInWindow + self.heightToKeyboard + self.frame.size.height) - self.keyboardY; 26 CGFloat moveHeight = height > 0 ? height : 0; 27 28 [UIView animateWithDuration:0.25 animations:^{ 29 // 判斷是否是scrollView併進行相應移動 30 if (self.hasContentOffset) { 31 UIScrollView *scrollView = (UIScrollView *)self.moveView; 32 scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, scrollView.contentOffset.y + moveHeight); 33 } else { 34 CGRect rect = self.moveView.frame; 35 self.initialY = rect.origin.y; 36 rect.origin.y -= moveHeight; 37 self.moveView.frame = rect; 38 } 39 // 記錄當前TextField使得moveView移動的距離 40 self.totalHeight += moveHeight; 41 }]; 42 } 43 44 - (void)hideKeyBoard:(CGFloat)duration { 45 [UIView animateWithDuration:duration animations:^{ 46 if (self.hasContentOffset) { 47 UIScrollView *scrollView = (UIScrollView *)self.moveView; 48 scrollView.contentOffset = CGPointMake(scrollView.contentOffset.x, scrollView.contentOffset.y - self.totalHeight); 49 } else { 50 CGRect rect = self.moveView.frame; 51 rect.origin.y += self.totalHeight; 52 self.moveView.frame = rect; 53 } 54 // moveView回覆狀態後將移動距離置0 55 self.totalHeight = 0; 56 }]; 57 }
點擊事件當前controllerview endediting
- (void)tapAction { [[self viewController].view endEditing:YES]; }