TPKeyboardAvoiding with Swift.
Node: Bài viết này mình chủ yếu là share code nhé. Mình tin rằng hầu hết các Develop ai cũng biết tới thư viện TPKeyboardAvoiding dùng để xử lý view khi keyboard ios và hiển thị. Dưới đây mình đã convert Code Objective - C sang Swift để dùng cho các dự án của mình. Source Code import ...
Node: Bài viết này mình chủ yếu là share code nhé. Mình tin rằng hầu hết các Develop ai cũng biết tới thư viện TPKeyboardAvoiding dùng để xử lý view khi keyboard ios và hiển thị.
Dưới đây mình đã convert Code Objective - C sang Swift để dùng cho các dự án của mình.
Source Code
import UIKit // MARK: - TableView class UITableViewSmart: UITableView { override var frame: CGRect { willSet(newValue) { if newValue.equalTo(frame) { return } super.frame = frame } didSet { if hasAutomaticKeyboardAvoidingBehaviour() {return} updateContentInset() } } override var contentSize: CGSize { willSet(newValue) { if newValue.equalTo(contentSize) { return } if hasAutomaticKeyboardAvoidingBehaviour() { super.contentSize = newValue return } super.contentSize = newValue } didSet { updateContentInset() } } override init(frame: CGRect, style: UITableViewStyle) { super.init(frame: frame, style: style) registerObserver() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) registerObserver() } override func awakeFromNib() { registerObserver() } deinit { removeObserver() } override func willMove(toSuperview newSuperview: UIView?) { super.willMove(toSuperview: newSuperview) if newSuperview != nil { cancelPerform() } } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { hideKeyBoard() super.touchesEnded(touches, with: event) } override func layoutSubviews() { super.layoutSubviews() callPerform() } } extension UITableViewSmart: UITextFieldDelegate { fileprivate func cancelPerform() { NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(assignTextDelegateForViewsBeneathView(_:)), object: self) } fileprivate func callPerform() { cancelPerform() Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(assignTextDelegateForViewsBeneathView(_:)), userInfo: nil, repeats: false) } func textFieldShouldReturn(_ textField: UITextField) -> Bool { if !focusNextTextField() { textField.resignFirstResponder() } return true } } extension UITableViewSmart: UITextViewDelegate { } extension UITableViewSmart { fileprivate func hideKeyBoard() { findFirstResponderBeneathView(self)?.resignFirstResponder() } fileprivate func hasAutomaticKeyboardAvoidingBehaviour()->Bool { if #available(iOS 8.3, *) { if self.delegate is UITableViewController { return true } } return false } fileprivate func focusNextTextField()->Bool { return keyboard_focusNextTextField() } fileprivate func removeObserver() { NotificationCenter.default.removeObserver(self) } fileprivate func registerObserver() { if hasAutomaticKeyboardAvoidingBehaviour() { return } NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(scrollToActiveTextField), name: NSNotification.Name.UITextViewTextDidBeginEditing, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(scrollToActiveTextField), name: NSNotification.Name.UITextFieldTextDidBeginEditing, object: nil) } } // MARK: - CollectionView class UICollectionViewSmart: UICollectionView { override var contentSize: CGSize { willSet(newValue) { if newValue.equalTo(contentSize) { return } super.contentSize = newValue updateContentInset() } } override var frame: CGRect{ willSet(newValue) { if newValue.equalTo(self.frame) { return } super.frame = frame updateContentInset() } } override init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) { super.init(frame: frame, collectionViewLayout: layout) registerObserver() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) registerObserver() } override func awakeFromNib() { super.awakeFromNib() registerObserver() } deinit { removeObserver() } override func willMove(toSuperview newSuperview: UIView?) { super.willMove(toSuperview: newSuperview) if newSuperview != nil { cancelPerform() } } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { hideKeyBoard() super.touchesEnded(touches, with: event) } override func layoutSubviews() { super.layoutSubviews() callPerform() } } extension UICollectionViewSmart: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { if !focusNextTextField() { textField.resignFirstResponder() } return true } } extension UICollectionViewSmart: UITextViewDelegate { } extension UICollectionViewSmart { fileprivate func cancelPerform() { NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(assignTextDelegateForViewsBeneathView(_:)), object: self) } fileprivate func callPerform() { cancelPerform() Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(assignTextDelegateForViewsBeneathView(_:)), userInfo: nil, repeats: false) } fileprivate func hideKeyBoard() { findFirstResponderBeneathView(self)?.resignFirstResponder() } fileprivate func focusNextTextField() -> Bool { return keyboard_focusNextTextField() } fileprivate func removeObserver() { NotificationCenter.default.removeObserver(self) } fileprivate func registerObserver() { NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(scrollToActiveTextField), name: NSNotification.Name.UITextViewTextDidBeginEditing, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(scrollToActiveTextField), name: NSNotification.Name.UITextFieldTextDidBeginEditing, object: nil) } } // MARK: - ScrollView class UIScrollViewSmart: UIScrollView { // MARK: - Property override var contentSize: CGSize { willSet(newValue) { if newValue.equalTo(contentSize) { return } super.contentSize = newValue updateFromContentSizeChange() } } override var frame: CGRect { willSet(newValue) { if newValue.equalTo(frame) { return } super.frame = newValue updateContentInset() } } // MARK: - LifeCycle override init(frame: CGRect) { super.init(frame: frame) registerObserver() } override func awakeFromNib() { super.awakeFromNib() registerObserver() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) registerObserver() } deinit { removeObserver() } // MARK: - Override override func willMove(toSuperview newSuperview: UIView?) { super.willMove(toSuperview: newSuperview) if newSuperview != nil { cancelPerform() } } override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) { hideKeyBoard() super.touchesEnded(touches, with: event) } override func layoutSubviews() { super.layoutSubviews() callPerform() } } // MARK: - Extension extension UIScrollViewSmart: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { if !focusNextTextField() { textField.resignFirstResponder() } return true } } extension UIScrollViewSmart: UITextViewDelegate { // TO DO } extension UIScrollViewSmart { fileprivate func cancelPerform() { NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(assignTextDelegateForViewsBeneathView(_:)), object: self) } /// register Delegate fileprivate func callPerform() { /// when call -> cancel cancelPerform() Timer.scheduledTimer(timeInterval: 0.1, target: self, selector: #selector(assignTextDelegateForViewsBeneathView(_:)), userInfo: nil, repeats: false) } fileprivate func hideKeyBoard() { findFirstResponderBeneathView(self)?.resignFirstResponder() } fileprivate func contentSizeToFit() { contentSize = calculatedContentSizeFromSubviewFrames() } fileprivate func focusNextTextField() -> Bool { return keyboard_focusNextTextField() } fileprivate func removeObserver() { NotificationCenter.default.removeObserver(self) } fileprivate func registerObserver() { /// UIKeyboardWillChangeFrame NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil) /// UIKeyboardWillHide NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil) /// UITextViewTextDidBeginEditing NotificationCenter.default.addObserver(self, selector: #selector(scrollToActiveTextField), name: NSNotification.Name.UITextViewTextDidBeginEditing, object: nil) /// UITextFieldTextDidBeginEditing NotificationCenter.default.addObserver(self, selector: #selector(scrollToActiveTextField), name: NSNotification.Name.UITextFieldTextDidBeginEditing, object: nil) } } // MARK: - Process Event let kCalculatedContentPadding: CGFloat = 10 let kMinimumScrollOffsetPadding: CGFloat = 20 extension UIScrollView { @objc fileprivate func keyboardWillShow(_ notification:Notification) { guard let userInfo = notification.userInfo else { return } guard let rectNotification = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue else { return } let keyboardRect = convert(rectNotification.cgRectValue , from: nil) if keyboardRect.isEmpty { return } let state = keyboardState() guard let firstResponder = findFirstResponderBeneathView(self) else { return} state.keyboardRect = keyboardRect if !state.keyboardVisible { state.priorInset = contentInset state.priorScrollIndicatorInsets = scrollIndicatorInsets state.priorPagingEnabled = isPagingEnabled } state.keyboardVisible = true isPagingEnabled = false if self is UIScrollViewSmart { state.priorContentSize = contentSize if contentSize.equalTo(CGSize.zero) { contentSize = calculatedContentSizeFromSubviewFrames() } } let duration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? Float ?? 0.0 let curve = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? Int ?? 0 let options = UIViewAnimationOptions(rawValue: UInt(curve)) UIView.animate(withDuration: TimeInterval(duration), delay: 0, options: options, animations: { [weak self]() -> Void in guard let strongSelf = self else { return } strongSelf.contentInset = strongSelf.contentInsetForKeyboard() let viewableHeight = strongSelf.bounds.size.height - (strongSelf.contentInset.top + strongSelf.contentInset.bottom) let point = CGPoint(x: strongSelf.contentOffset.x, y: strongSelf.idealOffsetForView(firstResponder, viewAreaHeight: viewableHeight)) strongSelf.setContentOffset(point, animated: false) strongSelf.scrollIndicatorInsets = strongSelf.contentInset strongSelf.layoutIfNeeded() }) { (finished) -> Void in } } @objc fileprivate func keyboardWillHide(_ notification: Notification) { guard let userInfo = notification.userInfo else { return } guard let rectNotification = userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue else { return } let keyboardRect = convert(rectNotification.cgRectValue , from: nil) if keyboardRect.isEmpty { return } let state = keyboardState() if !state.keyboardVisible { return } state.keyboardRect = CGRect.zero state.keyboardVisible = false let duration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as? Float ?? 0.0 let curve = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? Int ?? 0 let options = UIViewAnimationOptions(rawValue: UInt(curve)) UIView.animate(withDuration: TimeInterval(duration), delay: 0, options: options, animations: { [weak self]() -> Void in guard let strongSelf = self else { return } if strongSelf is UIScrollViewSmart { strongSelf.contentSize = state.priorContentSize strongSelf.contentInset = state.priorInset strongSelf.scrollIndicatorInsets = state.priorScrollIndicatorInsets strongSelf.isPagingEnabled = state.priorPagingEnabled strongSelf.layoutIfNeeded() } }) { (finished) -> Void in } } fileprivate func updateFromContentSizeChange() { let state = keyboardState() if state.keyboardVisible { state.priorContentSize = contentSize } } fileprivate func keyboard_focusNextTextField() -> Bool { guard let firstResponder = findFirstResponderBeneathView(self) else { return false} guard let view = findNextInputViewAfterView(firstResponder, beneathView: self) else { return false} Timer.scheduledTimer(timeInterval: 0.1, target: view, selector: #selector(becomeFirstResponder), userInfo: nil, repeats: false) return true } @objc fileprivate func scrollToActiveTextField() { let state = keyboardState() if !state.keyboardVisible { return } let visibleSpace = bounds.size.height - (contentInset.top + contentInset.bottom) let y = idealOffsetForView(findFirstResponderBeneathView(self), viewAreaHeight: visibleSpace) let idealOffset = CGPoint(x: 0, y: y) DispatchQueue.main.asyncAfter(deadline: .now() + Double((Int64)(0 * NSEC_PER_SEC)) / Double(NSEC_PER_SEC)) {[weak self] () -> Void in self?.setContentOffset(idealOffset, animated: true) } } fileprivate func findFirstResponderBeneathView(_ view:UIView) -> UIView? { for childView in view.subviews { if childView.responds(to: #selector(getter: isFirstResponder)) && childView.isFirstResponder { return childView } let result = findFirstResponderBeneathView(childView) if result != nil { return result } } return nil } fileprivate func updateContentInset() { let state = keyboardState() if state.keyboardVisible { contentInset = contentInsetForKeyboard() } } fileprivate func calculatedContentSizeFromSubviewFrames() ->CGSize { let wasShowingVerticalScrollIndicator = showsVerticalScrollIndicator let wasShowingHorizontalScrollIndicator = showsHorizontalScrollIndicator showsVerticalScrollIndicator = false showsHorizontalScrollIndicator = false var rect = CGRect.zero for view in self.subviews{ rect = rect.union(view.frame) } rect.size.height += kCalculatedContentPadding showsVerticalScrollIndicator = wasShowingVerticalScrollIndicator showsHorizontalScrollIndicator = wasShowingHorizontalScrollIndicator return rect.size } fileprivate func idealOffsetForView(_ view:UIView?,viewAreaHeight:CGFloat) -> CGFloat { let contentSize = self.contentSize var offset:CGFloat = 0.0 let subviewRect = view != nil ? view!.convert(view!.bounds, to: self) : CGRect.zero var padding = (viewAreaHeight - subviewRect.height)/2 if padding < kMinimumScrollOffsetPadding { padding = kMinimumScrollOffsetPadding } offset = subviewRect.origin.y - padding - self.contentInset.top if offset > (contentSize.height - viewAreaHeight) { offset = contentSize.height - viewAreaHeight } if offset < -contentInset.top { offset = -contentInset.top } return offset } fileprivate func contentInsetForKeyboard() -> UIEdgeInsets { let state = keyboardState() var newInset = contentInset let keyboardRect = state.keyboardRect newInset.bottom = keyboardRect.size.height - max(keyboardRect.maxY - bounds.maxY, 0) return newInset } fileprivate func viewIsValidKeyViewCandidate(_ view: UIView)->Bool { if view.isHidden || !view.isUserInteractionEnabled {return false} if view is UITextField { if (view as! UITextField).isEnabled {return true} } if view is UITextView { if (view as! UITextView).isEditable {return true} } return false } fileprivate func findNextInputViewAfterView(_ priorView:UIView,beneathView view:UIView, candidateView bestCandidate: inout UIView?) { let priorFrame = convert(priorView.frame, to: priorView.superview) let candidateFrame = bestCandidate == nil ? CGRect.zero : convert(bestCandidate!.frame, to: bestCandidate!.superview) var bestCandidateHeuristic = -sqrt(candidateFrame.origin.x*candidateFrame.origin.x + candidateFrame.origin.y*candidateFrame.origin.y) + ( Float(fabs(candidateFrame.minY - priorFrame.minY)) < .ulpOfOne ? 1e6 : 0) for childView in view.subviews { if viewIsValidKeyViewCandidate(childView) { let frame = convert(childView.frame, to: view) let heuristic = -sqrt(frame.origin.x*frame.origin.x + frame.origin.y*frame.origin.y) + (Float(fabs(frame.minY - priorFrame.minY)) < .ulpOfOne ? 1e6 : 0) if childView != priorView && (Float(fabs(frame.minY - priorFrame.minY)) < .ulpOfOne && frame.minX > priorFrame.minX || frame.minY > priorFrame.minY) && (bestCandidate == nil || heuristic > bestCandidateHeuristic) { bestCandidate = childView bestCandidateHeuristic = heuristic } } else { findNextInputViewAfterView(priorView, beneathView: view, candidateView: &bestCandidate) } } } fileprivate func findNextInputViewAfterView(_ priorView: UIView,beneathView view: UIView) ->UIView? { var candidate: UIView? findNextInputViewAfterView(priorView, beneathView: view, candidateView: &candidate) return candidate } @objc fileprivate func assignTextDelegateForViewsBeneathView(_ obj: AnyObject) { func processWithView(_ view: UIView) { for childView in view.subviews { if childView is UITextField || childView is UITextView { initializeView(childView) } else { assignTextDelegateForViewsBeneathView(childView) } } } if let timer = obj as? Timer, let view = timer.userInfo as? UIView { processWithView(view) } else if let view = obj as? UIView { processWithView(view) } } fileprivate func initializeView(_ view: UIView) { if let textField = view as? UITextField, let delegate = self as? UITextFieldDelegate, textField.returnKeyType == UIReturnKeyType.default && textField.delegate !== delegate { textField.delegate = delegate let otherView = findNextInputViewAfterView(view, beneathView: self) textField.returnKeyType = otherView != nil ? .next : .done } } fileprivate func keyboardState() -> KeyboardState { if let state = objc_getAssociatedObject(self, &AssociatedKeysKeyboard.key) as? KeyboardState { return state } else { let state = KeyboardState() self.state = state return state } } } fileprivate class KeyboardState { var priorInset = UIEdgeInsets.zero var priorScrollIndicatorInsets = UIEdgeInsets.zero var keyboardVisible = false var keyboardRect = CGRect.zero var priorContentSize = CGSize.zero var priorPagingEnabled = false } extension UIScrollView { fileprivate enum AssociatedKeysKeyboard { static var key = "KeyBoardSmart" } fileprivate var state: KeyboardState? { get { // get let optionalObject = objc_getAssociatedObject(self, &AssociatedKeysKeyboard.key) as AnyObject? if let object = optionalObject { return object as? KeyboardState } else { return nil } } set { // set objc_setAssociatedObject(self, &AssociatedKeysKeyboard.key, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) } } }
Hướng dẫn sủ dụng
mình sẽ hướng dẫn với UIScrollView nhé, các view khác tương tự. Các bạn Config giống như bài viết trước của mình.
với view trên, khi keyboard hiển thị thì sẽ che mất đi view nằm gần dưới bottom.
chỉnh UIScrollView là UIScrollViewSmart. thành quả.