Last active
April 10, 2020 12:23
-
-
Save KaQuMiQ/a0ec3ba4408f50d6e2af0339e96825ab to your computer and use it in GitHub Desktop.
Keyboard aware ViewController based on safeArea
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import UIKit | |
import ObjectiveC | |
private var originalAdditionalSafeAreaInsetsKey: Int = 0 | |
open class KeyboardAwareViewController: UIViewController { | |
open class var backgroundTapEditingEndHandlerEnabled: Bool { true } | |
private var originalAdditionalSafeAreaInsets: UIEdgeInsets? { | |
get { | |
objc_getAssociatedObject(self, &originalAdditionalSafeAreaInsetsKey) as? UIEdgeInsets | |
} | |
set { | |
objc_setAssociatedObject(self, &originalAdditionalSafeAreaInsetsKey, newValue, objc_AssociationPolicy.OBJC_ASSOCIATION_COPY_NONATOMIC) | |
} | |
} | |
deinit { | |
NotificationCenter.default.removeObserver(self) | |
} | |
override open func viewDidLoad() { | |
dispatchPrecondition(condition: .onQueue(.main)) | |
super.viewDidLoad() | |
setupKeyboardHandlers() | |
guard Self.backgroundTapEditingEndHandlerEnabled else { return } | |
setupBackgroundTapEditingEndHandler() | |
} | |
public func setupBackgroundTapEditingEndHandler() { | |
let backgroundTapGesture = UITapGestureRecognizer(target: self, action: #selector(backgroundTapEditingEndHandler)) | |
let backgroundTapGestureDelegate = BackgroundTapGestureDelegate(backgroundTapGesture: backgroundTapGesture) | |
backgroundTapGesture.delegate = backgroundTapGestureDelegate | |
objc_setAssociatedObject(self, &backgroundTapGestureDelegateKey, backgroundTapGestureDelegate, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC) | |
view.addGestureRecognizer(backgroundTapGesture) | |
} | |
private func setupKeyboardHandlers() { | |
NotificationCenter.default.addObserver(self, | |
selector: #selector(keyboardShow(notification:)), | |
name: UIResponder.keyboardWillShowNotification, object: nil) | |
NotificationCenter.default.addObserver(self, | |
selector: #selector(keyboardHide(notification:)), | |
name: UIResponder.keyboardWillHideNotification, object: nil) | |
} | |
@objc private func keyboardShow(notification: NSNotification) { | |
dispatchPrecondition(condition: .onQueue(.main)) | |
guard let notificationInfo = notification.userInfo else { return } | |
let keyboardInitialFrame = notificationInfo[UIResponder.keyboardFrameBeginUserInfoKey].flatMap { ($0 as? NSValue)?.cgRectValue } ?? .zero | |
let keyboardFinalFrame = notificationInfo[UIResponder.keyboardFrameEndUserInfoKey].flatMap { ($0 as? NSValue)?.cgRectValue } ?? .zero | |
guard keyboardFinalFrame != keyboardInitialFrame else { return } | |
let animationDuration = notificationInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double ?? 0.25 | |
let animationOptions = UIView.AnimationOptions(rawValue: notificationInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt ?? UInt(UIView.AnimationCurve.easeInOut.rawValue)) | |
if let originalAdditionalSafeAreaInsets = originalAdditionalSafeAreaInsets { | |
additionalSafeAreaInsets = originalAdditionalSafeAreaInsets | |
} else { | |
originalAdditionalSafeAreaInsets = additionalSafeAreaInsets | |
} | |
let overlappingKeyboardHeight: CGFloat | |
if let window = view.window { | |
overlappingKeyboardHeight = max(view.bounds.height - window.convert(keyboardFinalFrame, to: view).minY - view.safeAreaInsets.bottom, 0) | |
} else { | |
overlappingKeyboardHeight = max(keyboardFinalFrame.height - view.safeAreaInsets.bottom, 0) | |
} | |
additionalSafeAreaInsets = .init(top: 0, left: 0, bottom: overlappingKeyboardHeight + additionalSafeAreaInsets.bottom, right: 0) | |
view.setNeedsLayout() | |
viewSafeAreaInsetsDidChange() | |
UIView.animate( | |
withDuration: animationDuration, | |
delay: 0, | |
options: animationOptions, | |
animations: { | |
self.view.layoutIfNeeded() | |
} | |
) | |
} | |
@objc private func keyboardHide(notification: NSNotification) { | |
dispatchPrecondition(condition: .onQueue(.main)) | |
guard let notificationInfo = notification.userInfo else { return } | |
let keyboardInitialFrame = notificationInfo[UIResponder.keyboardFrameBeginUserInfoKey].flatMap { ($0 as? NSValue)?.cgRectValue } ?? .zero | |
let keyboardFinalFrame = notificationInfo[UIResponder.keyboardFrameEndUserInfoKey].flatMap { ($0 as? NSValue)?.cgRectValue } ?? .zero | |
guard keyboardFinalFrame != keyboardInitialFrame else { return } | |
let animationDuration = notificationInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double ?? 0.25 | |
let animationOptions = UIView.AnimationOptions(rawValue: notificationInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? UInt ?? UInt(UIView.AnimationCurve.easeInOut.rawValue)) | |
additionalSafeAreaInsets = originalAdditionalSafeAreaInsets ?? .zero | |
originalAdditionalSafeAreaInsets = nil | |
view.setNeedsLayout() | |
viewSafeAreaInsetsDidChange() | |
UIView.animate( | |
withDuration: animationDuration, | |
delay: 0, | |
options: animationOptions, | |
animations: { | |
self.view.layoutIfNeeded() | |
} | |
) | |
} | |
@objc private func backgroundTapEditingEndHandler() { | |
dispatchPrecondition(condition: .onQueue(.main)) | |
view.endEditing(true) | |
} | |
} | |
private var backgroundTapGestureDelegateKey: Int = 0 | |
private final class BackgroundTapGestureDelegate: NSObject, UIGestureRecognizerDelegate { | |
private weak var backgroundTapGesture: UIGestureRecognizer? | |
fileprivate init(backgroundTapGesture: UIGestureRecognizer?) { | |
self.backgroundTapGesture = backgroundTapGesture | |
} | |
fileprivate func gestureRecognizer( | |
_ gestureRecognizer: UIGestureRecognizer, | |
shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer | |
) -> Bool { | |
guard gestureRecognizer === backgroundTapGesture else { return true } | |
return false | |
} | |
fileprivate func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool { | |
return !(touch.view is UIControl) | |
} | |
fileprivate func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive press: UIPress) -> Bool { | |
false | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment