你們都用 IQKeyboardManager,git
IQKeyboardManager 引入,就管理好了github
從鍵盤事件中,獲得輸入文本框對象, UITextField / UITextView 的實例ide
IQKeyboardManager 初始化的時候,就完成了這些ui
有了文本框,要找到他當前的位置,framepwa
就要從文本框溯源,找到他的根視圖控制器code
而後計算出當前文本框在哪一個位置合適,orm
移動過去,就行了server
先算出,該文本框在根視圖的位置對象
再算出,該文本框在當前窗口, KeyWindow, 中的合適位置繼承
開始編輯,鍵盤出現,移動位置
結束編輯,鍵盤消失,還原位置
UIView 上放置幾個 UITextField / UITextView ,好處理
UIView 上放置 UITableView, UITableView 上的一個 cell,上面擺放 UITextField / UITextView,就複雜了一些
對於 UIAlertController 的輸入框,不用處理
比較特殊的,還有 UITableViewController、UISearchBar、
_UIAlertControllerTextFieldViewController
對於一個輸入框 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 作的工做,就複雜、全面了不少
註冊 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 }
輸入框開始編輯
從獲得的通知中,取得輸入框的對象,使用關聯的屬性,保存起來
// 註冊通知 @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 // 下面是一些,去除重複狀態的工做 }
// 鍵盤出現的通知 @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() } // ... }
經過屬性記錄,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 // 還有下移部分 }
上面的代碼
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
下
@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() } // ... }
控制器裏面,採用了自定製的頭部視圖,
鍵盤出現,自定製的頭部視圖不動,其餘部分照常動
用到的控制器,繼承自 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 } } }
controller.view
,一部分換成controller.virtualView
查看適合的位置,沿用
let textFieldViewRectInRootSuperview = textFieldView.superview?.convert(textFieldView.frame, to: rootController.view.superview)
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 } } }