我们在使用第三方框架时,往往需要import然后添加代码去实例化才能使用。但是IQKeyboardManager不需要任何代码就能自动解决键盘遮挡输入源,而且也提供了众多接口去实现各种需求。
当然我们可以选择在某些VC中代码中禁用:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[[IQKeyboardManager sharedManager] setEnable:NO];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[IQKeyboardManager sharedManager] setEnable:YES];
}
更统一优雅的做法禁用对应的类:
[[IQKeyboardManager sharedManager].disabledDistanceHandlingClasses addObject:[XXX class]];
1. 工程结构
- IQKeyboardManager 核心管理类,所有监听操作都在此进行
- IQKeyboardReturnKeyHandler 管理next/done等UI操作的处理事件
- Categories
- IQNSArray+Sort UIView.subviews排序的分类
- IQUIScrollView+Additions 为UIScrollView添加属性
- IQUITextFieldView+Additions 用于管理UITextField/UITextView的UIView分类
- IQUIView+Hierarchy UIView层级结构分类
- IQUIViewController+Additions vc添加NSLayoutConstraint的分类
- IQUIWindow+Hierarchy UIWindow层级的分类
- Constants
- IQKeyboardManagerConstants 枚举类型
- IQKeyboardManagerConstantsInternal IQLayoutGuidePosition枚举
- IQTextView
- IQTextView 实现UITextView支持placeholder的子类
- IQToolbar
- IQBarButtonItem 为IQToolbar使用item实现的UIBarButtonItem子类
- IQPreviousNextView IQPreviousNextView,复杂层级的情况下可以使用
- IQTitleBarButtonItem 带title的BarButtonItem
- IQToolbar UIToolbar的子类IQToolbar,即带3个按钮的toolbar
- IQUIView+IQKeyboardToolbar 在UIKeyboard添加IQToolbar的uiview分类
2. 实现流程
实际上,不需要我们去添加代码就能使用是重写了+(void)load方法。
//在程序启动之前会按(父类->子类->类别)的顺序调用所有的类的 +load 方法
+(void)load
{
//Enabling IQKeyboardManager. Loading asynchronous on main thread
[self performSelectorOnMainThread:@selector(sharedManager) withObject:nil waitUntilDone:NO];
}
紧接着实现了一个IQKeyboardManager的单例。同时对外禁用了init和new方法的创建方式。
/**
Unavailable. Please use sharedManager method
*/
-(nonnull instancetype)init NS_UNAVAILABLE;
/**
Unavailable. Please use sharedManager method
*/
+ (nonnull instancetype)new NS_UNAVAILABLE;
/* Singleton Object Initialization. */
-(instancetype)init
{
if (self = [super init])
{
__weak typeof(self) weakSelf = self;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
//为了防止多线程环境下self对象可能被释放的情况
__strong typeof(self) strongSelf = weakSelf;
//创建一个NSMutableSet用以装载UITextField和UITextView的类及子类
strongSelf.registeredClasses = [[NSMutableSet alloc] init];
//注册通知,包括4个键盘通知(UIKeyboardWillShowNotification,UIKeyboardDidShowNotification,UIKeyboardWillHideNotification,UIKeyboardDidHideNotification) 和 [UITextField class]以及[UITextView class] 的通知(UITextFieldTextDidBeginEditingNotification,UITextFieldTextDidEndEditingNotification;UITextViewTextDidBeginEditingNotification,UITextViewTextDidEndEditingNotification)
//以及方向改变(UIApplicationWillChangeStatusBarOrientationNotification)和状态栏frame改变(UIApplicationDidChangeStatusBarFrameNotification)的通知
[strongSelf registerAllNotifications];
//Creating gesture for @shouldResignOnTouchOutside. (Enhancement ID: #14)
//添加收键盘的手势
strongSelf.resignFirstResponderGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapRecognized:)];
//当手势冲突时,能够都传递响应
strongSelf.resignFirstResponderGesture.cancelsTouchesInView = NO;
[strongSelf.resignFirstResponderGesture setDelegate:self];
//根据属性值判断是否有效,默认是无效的,需要设置[IQKeyboardManager sharedManager].shouldResignOnTouchOutside = YES;(大神编程的思考方式值得我们学习)
strongSelf.resignFirstResponderGesture.enabled = strongSelf.shouldResignOnTouchOutside;
//Setting it's initial values
strongSelf.animationDuration = 0.25;
strongSelf.animationCurve = UIViewAnimationCurveEaseInOut;
//开启键盘不遮挡功能
[self setEnable:YES];
//设置text field到键盘的距离为10
[self setKeyboardDistanceFromTextField:10.0];
//当为yes时,点击next/previous/done 发出输入声响
[self setShouldPlayInputClicks:YES];
//设置点击其他地方收键盘无效
[self setShouldResignOnTouchOutside:NO];
//设置不为所以textField/textView设置keyboardAppearance,并且统一使用默认风格UIKeyboardAppearanceDefault
[self setOverrideKeyboardAppearance:NO];
[self setKeyboardAppearance:UIKeyboardAppearanceDefault];
//显示toolbar
[self setEnableAutoToolbar:YES];
//设置防止键盘管理器将rootView向上滑到键盘高度以上
[self setPreventShowingBottomBlankSpace:YES];
//设置在IQToolbar中添加textField占位符
[self setShouldShowToolbarPlaceholder:YES];
//设置根据Textfield的创建子视图的先后顺序,创建工具栏。详细参考responderViews,有3种模式:IQAutoToolbarBySubviews,IQAutoToolbarByTag,IQAutoToolbarByPosition
[self setToolbarManageBehaviour:IQAutoToolbarBySubviews];
//如果设置为YES,会在viewController's view每次改变frame时调用'setNeedsLayout' and 'layoutIfNeeded'
[self setLayoutIfNeededOnUpdate:NO];
// If YES, then always consider UINavigationController.view begin point as {0,0}, this is a workaround to fix a bug #464 because there are no notification mechanism exist when UINavigationController.view.frame gets changed internally.
[self setShouldFixInteractivePopGestureRecognizer:YES];
//Loading IQToolbar, IQTitleBarButtonItem, IQBarButtonItem to fix first time keyboard appearance delay (Bug ID: #550)
{
UITextField *view = [[UITextField alloc] init];
//添加done按钮
[view addDoneOnKeyboardWithTarget:nil action:nil];
//设置toolbar的next/previous/done
[view addPreviousNextDoneOnKeyboardWithTarget:nil previousAction:nil nextAction:nil doneAction:nil];
}
//Initializing disabled classes Set.
strongSelf.disabledDistanceHandlingClasses = [[NSMutableSet alloc] initWithObjects:[UITableViewController class],[UIAlertController class], nil];
strongSelf.enabledDistanceHandlingClasses = [[NSMutableSet alloc] init];
strongSelf.disabledToolbarClasses = [[NSMutableSet alloc] initWithObjects:[UIAlertController class], nil];
strongSelf.enabledToolbarClasses = [[NSMutableSet alloc] init];
strongSelf.toolbarPreviousNextAllowedClasses = [[NSMutableSet alloc] initWithObjects:[UITableView class],[UICollectionView class],[IQPreviousNextView class], nil];
strongSelf.disabledTouchResignedClasses = [[NSMutableSet alloc] initWithObjects:[UIAlertController class], nil];
strongSelf.enabledTouchResignedClasses = [[NSMutableSet alloc] init];
strongSelf.touchResignedGestureIgnoreClasses = [[NSMutableSet alloc] initWithObjects:[UIControl class],[UINavigationBar class], nil];
//如果为YES,使用textField的tintColor属性为IQToolbar的tintColor;为NO,IQToolbar的tintColor为黑色
[self setShouldToolbarUsesTextFieldTintColor:NO];
});
}
return self;
}
接下来我们打开[[IQKeyboardManager sharedManager] setEnableDebugging:YES];
来看看他的调用流程。
- 当弹出键盘时,会收到通知调用该方法:
#pragma mark - UITextFieldView Delegate methods
/** UITextFieldTextDidBeginEditingNotification, UITextViewTextDidBeginEditingNotification. Fetching UITextFieldView object. */
-(void)textFieldViewDidBeginEditing:(NSNotification*)notification
{
//获取当前CoreAnimation转化为秒的绝对时间
CFTimeInterval startTime = CACurrentMediaTime();
//使用_cmd打印当前方法名
[self showLog:[NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)]];
// 获取到该textfield并保存便于后续使用
_textFieldView = notification.object;
if (_overrideKeyboardAppearance == YES)
{
//如果实现了重写键盘样式
UITextField *textField = (UITextField*)_textFieldView;
if ([textField respondsToSelector:@selector(keyboardAppearance)])
{
//If keyboard appearance is not like the provided appearance
if (textField.keyboardAppearance != _keyboardAppearance)
{
//Setting textField keyboard appearance and reloading inputViews.
textField.keyboardAppearance = _keyboardAppearance;
[textField reloadInputViews];
}
}
}
//If autoToolbar enable, then add toolbar on all the UITextField/UITextView's if required.
if ([self privateIsEnableAutoToolbar])
{
//UITextView special case. Keyboard Notification is firing before textView notification so we need to reload it's inputViews.
if ([_textFieldView isKindOfClass:[UITextView class]] &&
_textFieldView.inputAccessoryView == nil)
{
__weak typeof(self) weakSelf = self;
[UIView animateWithDuration:0.00001 delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{
[self addToolbarIfRequired];
} completion:^(BOOL finished) {
__strong typeof(self) strongSelf = weakSelf;
//On textView toolbar didn't appear on first time, so forcing textView to reload it's inputViews.
[strongSelf.textFieldView reloadInputViews];
}];
}
//Else adding toolbar
else
{
//如果需要在textFields和它的同类中添加,则添加工具栏
//该方法会把层级中的textField和textview,IQDropDownTextField,IQTextView取出根据不同情况添加toolbar
[self addToolbarIfRequired];
}
}
else
{
//如果不添加则移除IQToolbar
[self removeToolbarIfRequired];
}
//Adding Geture recognizer to window (Enhancement ID: #14)
[_resignFirstResponderGesture setEnabled:[self privateShouldResignOnTouchOutside]];
[_textFieldView.window addGestureRecognizer:_resignFirstResponderGesture];
if ([self privateIsEnabled] == YES)
{
if (CGRectEqualToRect(_topViewBeginRect, CGRectZero)) // (Bug ID: #5)
{
// keyboard is not showing(At the beginning only). We should save rootViewRect and _layoutGuideConstraintInitialConstant.
_layoutGuideConstraint = [[_textFieldView viewController] IQLayoutGuideConstraint];
_layoutGuideConstraintInitialConstant = [_layoutGuideConstraint constant];
//获取到顶层vc
_rootViewController = [_textFieldView topMostController];
if (_rootViewController == nil) _rootViewController = [[self keyWindow] topMostWindowController];
_topViewBeginRect = _rootViewController.view.frame;
#ifdef __IPHONE_11_0
if (@available(iOS 11.0, *)) {
self.initialAdditionalSafeAreaInsets = _rootViewController.additionalSafeAreaInsets;
}
#endif
if (_topViewBeginRect.origin.y != 0 &&
_shouldFixInteractivePopGestureRecognizer &&
[_rootViewController isKindOfClass:[UINavigationController class]] &&
[_rootViewController modalPresentationStyle] != UIModalPresentationFormSheet &&
[_rootViewController modalPresentationStyle] != UIModalPresentationPageSheet)
{
UIWindow *window = [self keyWindow];
if (window)
{
_topViewBeginRect.origin.y = window.frame.size.height-_rootViewController.view.frame.size.height;
}
else
{
_topViewBeginRect.origin.y = 0;
}
}
[self showLog:[NSString stringWithFormat:@"Saving %@ beginning Frame: %@",[_rootViewController _IQDescription], NSStringFromCGRect(_topViewBeginRect)]];
}
//If _textFieldView is inside UIAlertView then do nothing. (Bug ID: #37, #74, #76)
//See notes:- https://developer.apple.com/library/ios/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html If it is UIAlertView textField then do not affect anything (Bug ID: #70).
if (_keyboardShowing == YES &&
_textFieldView != nil &&
[_textFieldView isAlertViewTextField] == NO)
{
// keyboard is already showing. adjust frame.
/** 1.首先获取KeyWindow
2.然后根据_textFieldView获取RootViewController
3.将_textFieldView.frame转换到keyWindow的坐标中
4.添加上keyboardDistanceFromTextField高度
5.根据多种情况重新设置_textFieldView所在vc的view的frame
如下,我们进入详细看看
**/
[self adjustFrame];
}
}
// if ([_textFieldView isKindOfClass:[UITextField class]])
// {
// [(UITextField*)_textFieldView addTarget:self action:@selector(editingDidEndOnExit:) forControlEvents:UIControlEventEditingDidEndOnExit];
// }
CFTimeInterval elapsedTime = CACurrentMediaTime() - startTime;
[self showLog:[NSString stringWithFormat:@"****** %@ ended: %g seconds ******",NSStringFromSelector(_cmd),elapsedTime]];
}
/* Adjusting RootViewController's frame according to interface orientation. */
-(void)adjustFrame
{
// We are unable to get textField object while keyboard showing on UIWebView's textField. (Bug ID: #11)
//作者提到了,如果是UIWebView中的textField,我们是获取不到的
if (_textFieldView == nil) return;
CFTimeInterval startTime = CACurrentMediaTime();
[self showLog:[NSString stringWithFormat:@"****** %@ started ******",NSStringFromSelector(_cmd)]];
// 获取keyWindow.
UIWindow *keyWindow = [self keyWindow];
// 获取_textFieldView所在的vc,如果不存在则获取keyWindow的 topController. (Bug ID: #1, #4)
UIViewController *rootController = [_textFieldView topMostController];
if (rootController == nil) rootController = [keyWindow topMostWindowController];
// 将_textFieldView的frame转换到keyWindow的坐标系中.
CGRect textFieldViewRect = [[_textFieldView superview] convertRect:_textFieldView.frame toView:keyWindow];
// Getting RootViewRect.
CGRect rootViewRect = [[rootController view] frame];
//Getting statusBarFrame
//Maintain keyboardDistanceFromTextField textfield到键盘的距离,默认为10
CGFloat specialKeyboardDistanceFromTextField = _textFieldView.keyboardDistanceFromTextField;
if (_textFieldView.isSearchBarTextField)
{
//如果包含UISearchBar
UISearchBar *searchBar = (UISearchBar*)[_textFieldView superviewOfClassType:[UISearchBar class]];
specialKeyboardDistanceFromTextField = searchBar.keyboardDistanceFromTextField;
}
CGFloat keyboardDistanceFromTextField = (specialKeyboardDistanceFromTextField == kIQUseDefaultKeyboardDistance)?_keyboardDistanceFromTextField:specialKeyboardDistanceFromTextField;
CGSize kbSize = _kbSize;
kbSize.height += keyboardDistanceFromTextField;
CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame];
// (Bug ID: #250)
IQLayoutGuidePosition layoutGuidePosition = IQLayoutGuidePositionNone;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
//If topLayoutGuide constraint
if (_layoutGuideConstraint && (_layoutGuideConstraint.firstItem == [[_textFieldView viewController] topLayoutGuide] ||
_layoutGuideConstraint.secondItem == [[_textFieldView viewController] topLayoutGuide]))
{
layoutGuidePosition = IQLayoutGuidePositionTop;
}
//If bottomLayoutGuice constraint
else if (_layoutGuideConstraint && (_layoutGuideConstraint.firstItem == [[_textFieldView viewController] bottomLayoutGuide] ||
_layoutGuideConstraint.secondItem == [[_textFieldView viewController] bottomLayoutGuide]))
{
layoutGuidePosition = IQLayoutGuidePositionBottom;
}
#pragma clang diagnostic pop
CGFloat topLayoutGuide = CGRectGetHeight(statusBarFrame);
CGFloat move = 0;
//move 大于0 隐藏; 小于0,显示
// +Move positive = textField is hidden.
// -Move negative = textField is showing.
// Checking if there is bottomLayoutGuide attached (Bug ID: #250)
if (layoutGuidePosition == IQLayoutGuidePositionBottom)
{
// Calculating move position.
move = CGRectGetMaxY(textFieldViewRect)-(CGRectGetHeight(keyWindow.frame)-kbSize.height);
}
else
{
// Calculating move position. Common for both normal and special cases.
move = MIN(CGRectGetMinY(textFieldViewRect)-(topLayoutGuide+5), CGRectGetMaxY(textFieldViewRect)-(CGRectGetHeight(keyWindow.frame)-kbSize.height));
}
[self showLog:[NSString stringWithFormat:@"Need to move: %.2f",move]];
UIScrollView *superScrollView = nil;
//查找当前输入框的父视图是否有ScrollView
UIScrollView *superView = (UIScrollView*)[_textFieldView superviewOfClassType:[UIScrollView class]];
//Getting UIScrollView whose scrolling is enabled. // (Bug ID: #285)
while (superView)
{
//判断是否可以滚动
if (superView.isScrollEnabled && superView.shouldIgnoreScrollingAdjustment == NO)
{
superScrollView = superView;
break;
}
else
{
//如果不能,则再取他的父视图,并且也是ScrollView才行
// Getting it's superScrollView. // (Enhancement ID: #21, #24)
superView = (UIScrollView*)[superView superviewOfClassType:[UIScrollView class]];
}
}
//If there was a lastScrollView. // (Bug ID: #34)
if (_lastScrollView)
{
//If we can't find current superScrollView, then setting lastScrollView to it's original form.
if (superScrollView == nil)
{
[self showLog:[NSString stringWithFormat:@"Restoring %@ contentInset to : %@ and contentOffset to : %@",[_lastScrollView _IQDescription],NSStringFromUIEdgeInsets(_startingContentInsets),NSStringFromCGPoint(_startingContentOffset)]];
__weak typeof(self) weakSelf = self;
[UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{
__strong typeof(self) strongSelf = weakSelf;
[strongSelf.lastScrollView setContentInset:strongSelf.startingContentInsets];
strongSelf.lastScrollView.scrollIndicatorInsets = strongSelf.startingScrollIndicatorInsets;
} completion:NULL];
if (_lastScrollView.shouldRestoreScrollViewContentOffset)
{
[_lastScrollView setContentOffset:_startingContentOffset animated:YES];
}
_startingContentInsets = UIEdgeInsetsZero;
_startingScrollIndicatorInsets = UIEdgeInsetsZero;
_startingContentOffset = CGPointZero;
_lastScrollView = nil;
}
//If both scrollView's are different, then reset lastScrollView to it's original frame and setting current scrollView as last scrollView.
else if (superScrollView != _lastScrollView)
{
[self showLog:[NSString stringWithFormat:@"Restoring %@ contentInset to : %@ and contentOffset to : %@",[_lastScrollView _IQDescription],NSStringFromUIEdgeInsets(_startingContentInsets),NSStringFromCGPoint(_startingContentOffset)]];
__weak typeof(self) weakSelf = self;
[UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{
__strong typeof(self) strongSelf = weakSelf;
[strongSelf.lastScrollView setContentInset:strongSelf.startingContentInsets];
strongSelf.lastScrollView.scrollIndicatorInsets = strongSelf.startingScrollIndicatorInsets;
} completion:NULL];
if (_lastScrollView.shouldRestoreScrollViewContentOffset)
{
[_lastScrollView setContentOffset:_startingContentOffset animated:YES];
}
_lastScrollView = superScrollView;
_startingContentInsets = superScrollView.contentInset;
_startingScrollIndicatorInsets = superScrollView.scrollIndicatorInsets;
_startingContentOffset = superScrollView.contentOffset;
[self showLog:[NSString stringWithFormat:@"Saving New %@ contentInset: %@ and contentOffset : %@",[_lastScrollView _IQDescription],NSStringFromUIEdgeInsets(_startingContentInsets),NSStringFromCGPoint(_startingContentOffset)]];
}
//Else the case where superScrollView == lastScrollView means we are on same scrollView after switching to different textField. So doing nothing
}
//If there was no lastScrollView and we found a current scrollView. then setting it as lastScrollView.
else if(superScrollView)
{
_lastScrollView = superScrollView;
_startingContentInsets = superScrollView.contentInset;
_startingContentOffset = superScrollView.contentOffset;
_startingScrollIndicatorInsets = superScrollView.scrollIndicatorInsets;
[self showLog:[NSString stringWithFormat:@"Saving %@ contentInset: %@ and contentOffset : %@",[_lastScrollView _IQDescription],NSStringFromUIEdgeInsets(_startingContentInsets),NSStringFromCGPoint(_startingContentOffset)]];
}
// Special case for ScrollView.
{
// If we found lastScrollView then setting it's contentOffset to show textField.
if (_lastScrollView)
{
//Saving
UIView *lastView = _textFieldView;
UIScrollView *superScrollView = _lastScrollView;
//Looping in upper hierarchy until we don't found any scrollView in it's upper hirarchy till UIWindow object.
while (superScrollView &&
(move>0?(move > (-superScrollView.contentOffset.y-superScrollView.contentInset.top)):superScrollView.contentOffset.y>0) )
{
UIScrollView *nextScrollView = nil;
UIScrollView *tempScrollView = (UIScrollView*)[superScrollView superviewOfClassType:[UIScrollView class]];
//Getting UIScrollView whose scrolling is enabled. // (Bug ID: #285)
while (tempScrollView)
{
if (tempScrollView.isScrollEnabled && tempScrollView.shouldIgnoreScrollingAdjustment == NO)
{
nextScrollView = tempScrollView;
break;
}
else
{
// Getting it's superScrollView. // (Enhancement ID: #21, #24)
tempScrollView = (UIScrollView*)[tempScrollView superviewOfClassType:[UIScrollView class]];
}
}
//Getting lastViewRect.
CGRect lastViewRect = [[lastView superview] convertRect:lastView.frame toView:superScrollView];
//Calculating the expected Y offset from move and scrollView's contentOffset.
CGFloat shouldOffsetY = superScrollView.contentOffset.y - MIN(superScrollView.contentOffset.y,-move);
//Rearranging the expected Y offset according to the view.
shouldOffsetY = MIN(shouldOffsetY, lastViewRect.origin.y/*-5*/); //-5 is for good UI.//Commenting -5 (Bug ID: #69)
//[_textFieldView isKindOfClass:[UITextView class]] If is a UITextView type
//[superScrollView superviewOfClassType:[UIScrollView class]] == nil If processing scrollView is last scrollView in upper hierarchy (there is no other scrollView upper hierrchy.)
//shouldOffsetY >= 0 shouldOffsetY must be greater than in order to keep distance from navigationBar (Bug ID: #92)
if ([_textFieldView isKindOfClass:[UITextView class]] &&
nextScrollView == nil &&
(shouldOffsetY >= 0))
{
CGFloat maintainTopLayout = 0;
//When uncommenting this, each calculation goes to well, but don't know why scrollView doesn't adjusting it's contentOffset at bottom
// if ([_textFieldView.viewController respondsToSelector:@selector(topLayoutGuide)])
// maintainTopLayout = [_textFieldView.viewController.topLayoutGuide length];
// else
maintainTopLayout = CGRectGetMaxY(_textFieldView.viewController.navigationController.navigationBar.frame);
maintainTopLayout+= 10; //For good UI
// Converting Rectangle according to window bounds.
CGRect currentTextFieldViewRect = [[_textFieldView superview] convertRect:_textFieldView.frame toView:keyWindow];
//Calculating expected fix distance which needs to be managed from navigation bar
CGFloat expectedFixDistance = CGRectGetMinY(currentTextFieldViewRect) - maintainTopLayout;
//Now if expectedOffsetY (superScrollView.contentOffset.y + expectedFixDistance) is lower than current shouldOffsetY, which means we're in a position where navigationBar up and hide, then reducing shouldOffsetY with expectedOffsetY (superScrollView.contentOffset.y + expectedFixDistance)
shouldOffsetY = MIN(shouldOffsetY, superScrollView.contentOffset.y + expectedFixDistance);
//Setting move to 0 because now we don't want to move any view anymore (All will be managed by our contentInset logic.
move = 0;
}
else
{
//Subtracting the Y offset from the move variable, because we are going to change scrollView's contentOffset.y to shouldOffsetY.
move -= (shouldOffsetY-superScrollView.contentOffset.y);
}
//Getting problem while using `setContentOffset:animated:`, So I used animation API.
[UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{
[self showLog:[NSString stringWithFormat:@"Adjusting %.2f to %@ ContentOffset",(superScrollView.contentOffset.y-shouldOffsetY),[superScrollView _IQDescription]]];
[self showLog:[NSString stringWithFormat:@"Remaining Move: %.2f",move]];
superScrollView.contentOffset = CGPointMake(superScrollView.contentOffset.x, shouldOffsetY);
} completion:NULL];
// Getting next lastView & superScrollView.
lastView = superScrollView;
superScrollView = nextScrollView;
}
//Updating contentInset
{
CGRect lastScrollViewRect = [[_lastScrollView superview] convertRect:_lastScrollView.frame toView:keyWindow];
CGFloat bottom = kbSize.height-keyboardDistanceFromTextField-(CGRectGetHeight(keyWindow.frame)-CGRectGetMaxY(lastScrollViewRect));
// Update the insets so that the scroll vew doesn't shift incorrectly when the offset is near the bottom of the scroll view.
UIEdgeInsets movedInsets = _lastScrollView.contentInset;
movedInsets.bottom = MAX(_startingContentInsets.bottom, bottom);
[self showLog:[NSString stringWithFormat:@"%@ old ContentInset : %@",[_lastScrollView _IQDescription], NSStringFromUIEdgeInsets(_lastScrollView.contentInset)]];
__weak typeof(self) weakSelf = self;
[UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{
__strong typeof(self) strongSelf = weakSelf;
strongSelf.lastScrollView.contentInset = movedInsets;
UIEdgeInsets newInset = strongSelf.lastScrollView.scrollIndicatorInsets;
newInset.bottom = movedInsets.bottom;
strongSelf.lastScrollView.scrollIndicatorInsets = newInset;
} completion:NULL];
[self showLog:[NSString stringWithFormat:@"%@ new ContentInset : %@",[_lastScrollView _IQDescription], NSStringFromUIEdgeInsets(_lastScrollView.contentInset)]];
}
}
//Going ahead. No else if.
}
if (layoutGuidePosition == IQLayoutGuidePositionTop)
{
CGFloat constant = MIN(_layoutGuideConstraintInitialConstant, _layoutGuideConstraint.constant-move);
__weak typeof(self) weakSelf = self;
[UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{
__strong typeof(self) strongSelf = weakSelf;
weakSelf.layoutGuideConstraint.constant = constant;
[strongSelf.rootViewController.view setNeedsLayout];
[strongSelf.rootViewController.view layoutIfNeeded];
} completion:NULL];
}
//If bottomLayoutGuice constraint
else if (layoutGuidePosition == IQLayoutGuidePositionBottom)
{
CGFloat constant = MAX(_layoutGuideConstraintInitialConstant, _layoutGuideConstraint.constant+move);
__weak typeof(self) weakSelf = self;
[UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{
__strong typeof(self) strongSelf = weakSelf;
weakSelf.layoutGuideConstraint.constant = constant;
[strongSelf.rootViewController.view setNeedsLayout];
[strongSelf.rootViewController.view layoutIfNeeded];
} completion:NULL];
}
//If not constraint
else
{
//Special case for UITextView(Readjusting textView.contentInset when textView hight is too big to fit on screen)
//_lastScrollView If not having inside any scrollView, (now contentInset manages the full screen textView.
//[_textFieldView isKindOfClass:[UITextView class]] If is a UITextView type
if ([_textFieldView isKindOfClass:[UITextView class]])
{
UITextView *textView = (UITextView*)_textFieldView;
CGFloat textViewHeight = MIN(CGRectGetHeight(_textFieldView.frame), (CGRectGetHeight(keyWindow.frame)-kbSize.height-(topLayoutGuide)));
if (_textFieldView.frame.size.height-textView.contentInset.bottom>textViewHeight)
{
__weak typeof(self) weakSelf = self;
[UIView animateWithDuration:_animationDuration delay:0 options:(_animationCurve|UIViewAnimationOptionBeginFromCurrentState) animations:^{
__strong typeof(self) strongSelf = weakSelf;
[self showLog:[NSString stringWithFormat:@"%@ Old UITextView.contentInset : %@",[strongSelf.textFieldView _IQDescription], NSStringFromUIEdgeInsets(textView.contentInset)]];
//_isTextViewContentInsetChanged, If frame is not change by library in past, then saving user textView properties (Bug ID: #92)
if (strongSelf.isTextViewContentInsetChanged == NO)
{
strongSelf.startingTextViewContentInsets = textView.contentInset;
strongSelf.startingTextViewScrollIndicatorInsets = textView.scrollIndicatorInsets;
}
UIEdgeInsets newContentInset = textView.contentInset;
newContentInset.bottom = strongSelf.textFieldView.frame.size.height-textViewHeight;
textView.contentInset = newContentInset;
textView.scrollIndicatorInsets = newContentInset;
strongSelf.isTextViewContentInsetChanged = YES;
[self showLog:[NSString stringWithFormat:@"%@ New UITextView.contentInset : %@",[strongSelf.textFieldView _IQDescription], NSStringFromUIEdgeInsets(textView.contentInset)]];
} completion:NULL];
}
}
// Special case for iPad modalPresentationStyle.
if ([rootController modalPresentationStyle] == UIModalPresentationFormSheet ||
[rootController modalPresentationStyle] == UIModalPresentationPageSheet)
{
[self showLog:[NSString stringWithFormat:@"Found Special case for Model Presentation Style: %ld",(long)(rootController.modalPresentationStyle)]];
// +Positive or zero.
if (move>=0)
{
// We should only manipulate y.
rootViewRect.origin.y -= move;
// From now prevent keyboard manager to slide up the rootView to more than keyboard height. (Bug ID: #93)
if (_preventShowingBottomBlankSpace == YES)
{
CGFloat minimumY = (CGRectGetHeight(keyWindow.frame)-rootViewRect.size.height-topLayoutGuide)/2-(kbSize.height-keyboardDistanceFromTextField);
rootViewRect.origin.y = MAX(rootViewRect.origin.y, minimumY);
}
[self showLog:@"Moving Upward"];
// Setting adjusted rootViewRect
[self setRootViewFrame:rootViewRect];
_movedDistance = (_topViewBeginRect.origin.y-rootViewRect.origin.y);
}
// -Negative
else
{
// Calculating disturbed distance. Pull Request #3
CGFloat disturbDistance = CGRectGetMinY(rootViewRect)-CGRectGetMinY(_topViewBeginRect);
// disturbDistance Negative = frame disturbed.
// disturbDistance positive = frame not disturbed.
if(disturbDistance<=0)
{
// We should only manipulate y.
rootViewRect.origin.y -= MAX(move, disturbDistance);
[self showLog:@"Moving Downward"];
// Setting adjusted rootViewRect
[self setRootViewFrame:rootViewRect];
_movedDistance = (_topViewBeginRect.origin.y-rootViewRect.origin.y);
}
}
}
//If presentation style is neither UIModalPresentationFormSheet nor UIModalPresentationPageSheet then going ahead.(General case)
else
{
// +Positive or zero.
if (move>=0)
{
rootViewRect.origin.y -= move;
// From now prevent keyboard manager to slide up the rootView to more than keyboard height. (Bug ID: #93)
if (_preventShowingBottomBlankSpace == YES)
{
rootViewRect.origin.y = MAX(rootViewRect.origin.y, MIN(0, -kbSize.height+keyboardDistanceFromTextField));
}
[self showLog:@"Moving Upward"];
// Setting adjusted rootViewRect
[self setRootViewFrame:rootViewRect];
_movedDistance = (_topViewBeginRect.origin.y-rootViewRect.origin.y);
}
// -Negative
else
{
CGFloat disturbDistance = CGRectGetMinY(rootViewRect)-CGRectGetMinY(_topViewBeginRect);
// disturbDistance Negative = frame disturbed. Pull Request #3
// disturbDistance positive = frame not disturbed.
if(disturbDistance<=0)
{
rootViewRect.origin.y -= MAX(move, disturbDistance);
[self showLog:@"Moving Downward"];
// Setting adjusted rootViewRect
//设置 [controller.view setFrame:frame];
[self setRootViewFrame:rootViewRect];
_movedDistance = (_topViewBeginRect.origin.y-rootViewRect.origin.y);
}
}
}
}
CFTimeInterval elapsedTime = CACurrentMediaTime() - startTime;
[self showLog:[NSString stringWithFormat:@"****** %@ ended: %g seconds ******",NSStringFromSelector(_cmd),elapsedTime]];
}
-
当从一个textview/textfield切换到另一个textview/textfield。
-
手动点击切换:
#pragma mark - UITextFieldView Delegate methods /** UITextFieldTextDidBeginEditingNotification, UITextViewTextDidBeginEditingNotification. Fetching UITextFieldView object. */ -(void)textFieldViewDidBeginEditing:(NSNotification*)notification { /** 1.如果给textField/textView重写Appearance,_overrideKeyboardAppearance,则设置 2.如果需要添加toolbar,则添加,并加上对应的action 3.添加_resignFirstResponderGesture手势到_textFieldView.window;并判断privateIsEnabled是否允许以及响应区域 4.keyboard显示之后,调整vc的frame **/ }
-
如果是点击ToolBar跳转:
//该方法添加对应的action [textField addPreviousNextDoneOnKeyboardWithTarget:self previousAction:@selector(previousAction:) nextAction:@selector(nextAction:) doneAction:@selector(doneAction:) shouldShowPlaceholder:_shouldShowToolbarPlaceholder];
以previousAction为例。
-(void)previousAction:(IQBarButtonItem*)barButton { //如果需要播放声音,则播放 if (_shouldPlayInputClicks) { [[UIDevice currentDevice] playInputClick]; } //拿到所以可以响应的view,判断当前textField如果不是第一个,则可以此操作,否则返回NO if ([self canGoPrevious]) { UIView *currentTextFieldView = _textFieldView; //拿到响应的textview数组,取出当前的上一个,并判断是否能够成为第一响应者 //成为第一响应者同时,会监听到textFieldViewDidBeginEditing 和keyboardWillShow通知 BOOL isAcceptAsFirstResponder = [self goPrevious]; if (isAcceptAsFirstResponder == YES && barButton.invocation) { //当有多个参数调用时,会使用到invocation;系统的NSObject提供的performSelector:withObject的方法只提供了最多两个参数的调用 //NSInvocation 是命令模式的一种实现,它包含选择器、方法签名、相应的参数以及目标对象 if (barButton.invocation.methodSignature.numberOfArguments > 2) { [barButton.invocation setArgument:¤tTextFieldView atIndex:2]; } [barButton.invocation invoke]; } } }
-
-
最后是收键盘,调用实现的resignFirstResponder方法,并且监听keyboardWillHide以及keyboardDidHide。
/** Resigning textField. */ - (BOOL)resignFirstResponder { if (_textFieldView) { // Retaining textFieldView UIView *textFieldRetain = _textFieldView; //Resigning first responder BOOL isResignFirstResponder = [_textFieldView resignFirstResponder]; // If it refuses then becoming it as first responder again. (Bug ID: #96) if (isResignFirstResponder == NO) { //If it refuses to resign then becoming it first responder again for getting notifications callback. [textFieldRetain becomeFirstResponder]; [self showLog:[NSString stringWithFormat:@"Refuses to Resign first responder: %@",[_textFieldView _IQDescription]]]; } return isResignFirstResponder; } else { return NO; } }
//在该方法中将rootViewController还原为原来的frame - (void)keyboardWillHide:(NSNotification*)aNotification //在该方法中将用于保存rootViewController.view.frame的_topViewBeginRect置为CGRectZero - (void)keyboardDidHide:(NSNotification*)aNotification
至此就结束了。
3. 总结
大体流程如下图:
首先从+(void)load方法加载单例开始。
然后分别注册UIKeyboard,UITextview,UITextfield,Orientation通知。
UIKeyboardWillShow:首先保存noti到 kbShowNotification;然后判断KeyboardManager是否enable,NO则结束;YES则获取keyboard通知属性,判断是否Default动画,NO则保存AnimationCurve到 animationCurve;保存 oldSize of kbsize到oldkbSize,保存keyboardSize到_kbSize;判断kbSize和oldKBSize是否相等,相等则调整Frame。
adjust frame 首先会判断当前_textFieldView是否为空,因为如果为webview是获取不到textfield的。不为空则设置 _iskeyboardShowing 为YES;获取KeyWindow,获取window上的TopMostController保存为rootcontroller;一顿操作获取到move的值;然后判断当前textfield是否有superScorllview,如果找到则调整superScorllview的contentOffset;没有找到则通过Move值判断,如果大于0,则setRootViewFrame向下;如果小于等于0,则RootVC向上偏移。
UITextFieldTextDidBeginEditingNotification/UITextViewTextDidBeginEditingNotification:则是添加UIToolbar作为inputAccessoryView。首先会将当前textview保存到_textFieldView;然后判断几个属性做相应的设置,比如KeyboardAppearance,是否添加收键盘手势,是否允许添加toolbar,当然这里允许才会继续往下。然后会执行AddToolbarIfRequired;他先是会去找到所有的Textfield/textView,然后根据不同的策略排序;然后判断是否有InputAccessoryView,没有则创建;有的话则根据当前的是第一个还是最后一个去禁用对应的Next/Previous。最后判断 _kbSize, oldKBSize是否相等,不相同则调用adjustFrame刷新。
UIApplicationWillChangeStatusBarOrientationNotification: 如果textViewContentInsetChanged为NO则结束;YES则设置设置 textFieldView.frame为_textFieldViewIntialFrame。
-
UIKeyboardWillHide: 分别保存_textFieldFrame到TextFieldViewIntialFrame;将lastScorllview置为nil,kbSize置为CGSizeZero,并将rootViewController的frame还原。
详细逻辑可以参考下图: