Skip to content

Instantly share code, notes, and snippets.

@KaQuMiQ
Last active April 10, 2020 12:23
Show Gist options
  • Save KaQuMiQ/a0ec3ba4408f50d6e2af0339e96825ab to your computer and use it in GitHub Desktop.
Save KaQuMiQ/a0ec3ba4408f50d6e2af0339e96825ab to your computer and use it in GitHub Desktop.
Keyboard aware ViewController based on safeArea
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