IQKeyboardManager 三步走
大家都用 IQKeyboardManager,
IQKeyboardManager 引入,就管理好了
第 1 步,注册系统通知,获得键盘事件
从键盘事件中,得到输入文本框对象, UITextField / UITextView 的实例
IQKeyboardManager 初始化的时候,就完成了这些
第 2 步,计算出当前文本框的位置, 并移动
有了文本框,要找到他当前的位置,frame
就要从文本框溯源,找到他的根视图控制器
然后计算出当前文本框在哪个位置合适,
移动过去,就好了
2.1 , 计算出合适的位置
先算出,该文本框在根视图的位置
再算出,该文本框在当前窗口, KeyWindow, 中的合适位置
2.2,键盘出现,与键盘消失
开始编辑,键盘出现,移动位置
结束编辑,键盘消失,还原位置
3,情况判断
UIView 上放置几个 UITextField / UITextView ,好处理
UIView 上放置 UITableView, UITableView 上的一个 cell,上面摆放 UITextField / UITextView,就复杂了一些
3.1 特殊类处理,
对于 UIAlertController 的输入框,不用处理
比较特殊的,还有 UITableViewController、UISearchBar、
_UIAlertControllerTextFieldViewController
0, 键盘管理,很简单
对于一个输入框 UITextField , 放置在 UIView 上,
键盘出来了,这个 UITextField 的位置,要适当,
通过两个通知处理掉,
一般情况下,键盘出来,把 UITextField 位置放高一点,
键盘消失,把 UITextField 位置放回原处
import SnapKit
// 注册通知
func config(){
NotificationCenter.default.addObserver(self,
selector: #selector(self.keyboardWillShow(noti:)),
name: UIWindow.keyboardWillShowNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(self.keyboardWillHide(noti:)),
name: UIWindow.keyboardWillHideNotification,
object: nil)
}
// 放高一点
@objc
func keyboardWillShow(noti notification: NSNotification){
yConstraint?.constraint.update(offset: s.height * (-0.5))
layoutIfNeeded()
}
// 放回原处
@objc
func keyboardWillHide(noti notification: NSNotification){
yConstraint?.constraint.update(offset: 0)
layoutIfNeeded()
}
对于 UITextView,也这样处理
IQKeyboardManager 做的工作,就复杂、全面了很多
1, 初始化工作
注册 4 个键盘通知,
键盘将要出现,键盘出现了,
键盘将要消失,键盘消失了,
输入文本框,有两种,UITextField 和 UITextView
再注册两个 UITextField 的通知,两个 UITextView 的通知
最后注册一个屏幕旋转的通知
@objc func registerAllNotifications() {
// Registering for keyboard notification.
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow(_:)), name: UIKeyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardDidShow(_:)), name: UIKeyboardDidShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide(_:)), name: UIKeyboardWillHideNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardDidHide(_:)), name: UIKeyboardDidHideNotification, object: nil)
// Registering for UITextField notification.
registerTextFieldViewClass(UITextField.self, didBeginEditingNotificationName: UITextFieldTextDidBeginEditingNotification.rawValue, didEndEditingNotificationName: UITextFieldTextDidEndEditingNotification.rawValue)
// Registering for UITextView notification.
registerTextFieldViewClass(UITextView.self, didBeginEditingNotificationName: UITextViewTextDidBeginEditingNotification.rawValue, didEndEditingNotificationName: UITextViewTextDidEndEditingNotification.rawValue)
// Registering for orientation changes notification
NotificationCenter.default.addObserver(self, selector: #selector(self.willChangeStatusBarOrientation(_:)), name: UIApplicationWillChangeStatusBarOrientationNotification, object: UIApplication.shared)
}
我们使用自动键盘,这样来一下
IQKeyboardManager.shared.enable = true
激活了内部的功能使用,状态判断
func privateIsEnabled() -> Bool {
// 这里用到了,我们设置的属性
var isEnabled = enable
let enableMode = textFieldView?.enableMode
if enableMode == .enabled {
isEnabled = true
} else if enableMode == .disabled {
isEnabled = false
} else if var textFieldViewController = textFieldView?.viewContainingController() {
// 走这里
//If it is searchBar textField embedded in Navigation Bar
if textFieldView?.textFieldSearchBar() != nil, let navController = textFieldViewController as? UINavigationController, let topController = navController.topViewController {
textFieldViewController = topController
}
//If viewController is kind of enable viewController class, then assuming it's enabled.
if !isEnabled, enabledDistanceHandlingClasses.contains(where: { textFieldViewController.isKind(of: $0) }) {
isEnabled = true
}
// ...
}
// 这里起作用
return isEnabled
}
2, 计算位置,与修改
2.1, 点击一个文本框 UITextField,先走输入框通知的方法
输入框开始编辑
从得到的通知中,取得输入框的对象,使用关联的属性,保存起来
// 注册通知
@objc public func registerTextFieldViewClass(_ aClass: UIView.Type, didBeginEditingNotificationName: String, didEndEditingNotificationName: String) {
NotificationCenter.default.addObserver(self, selector: #selector(self.textFieldViewDidBeginEditing(_:)), name: Notification.Name(rawValue: didBeginEditingNotificationName), object: nil)
// ...
}
//
@objc func textFieldViewDidBeginEditing(_ notification: Notification) {
let startTime = CACurrentMediaTime()
showLog("****** \(#function) started ******", indentation: 1)
// Getting object
textFieldView = notification.object as? UIView
// 下面是一些,去除重复状态的工作
}
2.2, 再走键盘将要出现的方法
// 键盘出现的通知
@objc func registerAllNotifications() {
// Registering for keyboard notification.
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow(_:)), name: UIKeyboardWillShowNotification, object: nil)
// ...
}
// 键盘出现,调整位置
@objc internal func keyboardWillShow(_ notification: Notification?) {
// ...
if keyboardShowing,
let textFieldView = textFieldView,
textFieldView.isAlertViewTextField() == false {
// keyboard is already showing. adjust position.
// 调整位置
optimizedAdjustPosition()
}
// ...
}
2.3, 去调整位置
通过属性记录,hasPendingAdjustRequest
, 一次调整必须完成,再来下一个
internal func optimizedAdjustPosition() {
if !hasPendingAdjustRequest {
hasPendingAdjustRequest = true
OperationQueue.main.addOperation {
self.adjustPosition()
self.hasPendingAdjustRequest = false
}
}
}
先做坐标转换,
找出合适的移动距离,
再判断情况,
最后去移动,
移动就是把父视图的坐标原点改下,与 前面的 0 ,键盘管理 一样
private func adjustPosition() {
// 坐标转换
// We are unable to get textField object while keyboard showing on WKWebView's textField. (Bug ID: #11)
guard hasPendingAdjustRequest,
let textFieldView = textFieldView,
let rootController = textFieldView.parentContainerViewController(),
let window = keyWindow(),
let textFieldViewRectInWindow = textFieldView.superview?.convert(textFieldView.frame, to: window),
let textFieldViewRectInRootSuperview = textFieldView.superview?.convert(textFieldView.frame, to: rootController.view?.superview) else {
return
}
// ...
// 计算初步移动距离
var move: CGFloat = min(textFieldViewRectInRootSuperview.minY-(topLayoutGuide), textFieldViewRectInWindow.maxY-(window.frame.height-kbSize.height)+bottomLayoutGuide)
// 情况判断
// ...
// 上移部分
// +Positive or zero.
if move >= 0 {
rootViewOrigin.y = max(rootViewOrigin.y - move, min(0, -(kbSize.height-newKeyboardDistanceFromTextField)))
if rootController.view.frame.origin.equalTo(rootViewOrigin) == false {
showLog("Moving Upward")
UIView.animate(withDuration: animationDuration, delay: 0, options: animationCurve, animations: { () -> Void in
var rect = rootController.view.frame
rect.origin = rootViewOrigin
rootController.view.frame = rect
//Animating content if needed (Bug ID: #204)
if self.layoutIfNeededOnUpdate {
//Animating content (Bug ID: #160)
rootController.view.setNeedsLayout()
rootController.view.layoutIfNeeded()
}
self.showLog("Set \(rootController) origin to: \(rootViewOrigin)")
})
}
movedDistance = (topViewBeginOrigin.y-rootViewOrigin.y)
} else { // -Negative
// 还有下移部分
}
2.4 坐标转换,找出位置
上面的代码
guard hasPendingAdjustRequest,
let textFieldView = textFieldView,
let rootController = textFieldView.parentContainerViewController(),
let textFieldViewRectInRootSuperview = textFieldView.superview?.convert(textFieldView.frame, to: rootController.view?.superview) else {
return
}
拿到了一个输入框 UITextField / UITextView,
先取得其父响应者中,第一个视图控制器,
拿到第一个视图控制器, 找到当前根视图控制器,
( 因为可能一个控制器的子控制器的视图上,放了一个文本框 )
@objc public extension UIView {
// 从输入框,到控制器
func viewContainingController() -> UIViewController? {
var nextResponder: UIResponder? = self
repeat {
nextResponder = nextResponder?.next
if let viewController = nextResponder as? UIViewController {
return viewController
}
} while nextResponder != nil
return nil
}
// 从控制器,到当前根视图控制器,
// 这个方法,没有使用递归,只是简单的往前翻
func parentContainerViewController() -> UIViewController? {
var matchController = viewContainingController()
var parentContainerViewController: UIViewController?
if var navController = matchController?.navigationController {
while let parentNav = navController.navigationController {
navController = parentNav
}
var parentController: UIViewController = navController
while let parent = parentController.parent,
(parent.isKind(of: UINavigationController.self) == false &&
parent.isKind(of: UITabBarController.self) == false &&
parent.isKind(of: UISplitViewController.self) == false) {
parentController = parent
}
if navController == parentController {
parentContainerViewController = navController.topViewController
} else {
parentContainerViewController = parentController
}
} else if let tabController = matchController?.tabBarController {
if let navController = tabController.selectedViewController as? UINavigationController {
parentContainerViewController = navController.topViewController
} else {
parentContainerViewController = tabController.selectedViewController
}
} else {
while let parentController = matchController?.parent,
(parentController.isKind(of: UINavigationController.self) == false &&
parentController.isKind(of: UITabBarController.self) == false &&
parentController.isKind(of: UISplitViewController.self) == false) {
matchController = parentController
}
parentContainerViewController = matchController
}
let finalController = parentContainerViewController?.parentIQContainerViewController() ?? parentContainerViewController
return finalController
}
}
坐标转换,找出位置,简单的 convert
下
2.4, 再走键盘出现了的方法
@objc internal func keyboardDidShow(_ notification: Notification?) {
// 功能激活控制
if privateIsEnabled() == false {
return
}
let startTime = CACurrentMediaTime()
showLog("****** \(#function) started ******", indentation: 1)
if let textFieldView = _textFieldView,
let parentController = textFieldView.parentContainerViewController(), (parentController.modalPresentationStyle == UIModalPresentationStyle.formSheet || parentController.modalPresentationStyle == UIModalPresentationStyle.pageSheet){
self.optimizedAdjustPosition()
}
// ...
}
3 应用: ( 情况判断部分,略)
控制器里面,采用了自定制的头部视图,
键盘出现,自定制的头部视图不动,其他部分照常动
3.1 解决
用到的控制器,继承自 BaseC
,
键盘相关的视图,添加在 contentView
上
class BaseC: UIViewController {
var contentView :UIView = {
let w = UIView()
w.backgroundColor = .clear
return w
}()
var firstLoad = true
var contentFrame: CGRect{
get{
contentView.frame
}
set{
contentView.frame = newValue
}
}
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(contentView)
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if firstLoad{
contentView.frame = view.frame
firstLoad = false
}
}
}
- IQKeyboardManager 原来使用的
controller.view
,一部分换成
controller.virtualView
查看适合的位置,沿用
let textFieldViewRectInRootSuperview = textFieldView.superview?.convert(textFieldView.frame, to: rootController.view.superview)
- IQKeyboardManager 原来修改位置的
controller.view.frame =
,
换成 controller.containerFrame
extension UIViewController{
var containerFrame: CGRect{
get{
if let me = self as? BaseC{
return me.contentFrame
}
else{
return view.frame
}
}
set{
if let me = self as? BaseC{
me.contentFrame = newValue
}
else{
view.frame = newValue
}
}
}
var virtualView: UIView{
if let v = self as? BaseC{
return v.contentView
}
else{
return view
}
}
}