Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save vinczebalazs/48e62aded1c88bbcda47ef80d4bba407 to your computer and use it in GitHub Desktop.
Save vinczebalazs/48e62aded1c88bbcda47ef80d4bba407 to your computer and use it in GitHub Desktop.
A simple UIViewController subclass that avoids the keyboard automatically.
import UIKit
class KeyboardAvoidingViewController: UIViewController {
private let margin: CGFloat = 20
private var keyboardFrame: CGRect?
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,
selector: #selector(adjustForKeyboard),
name: UIResponder.keyboardWillHideNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(adjustForKeyboard),
name: UIResponder.keyboardWillChangeFrameNotification,
object: nil)
}
@objc
private func adjustForKeyboard(notification: Notification) {
guard notification.name != UIResponder.keyboardWillHideNotification else {
keyboardFrame = nil
view.frame.origin.y = 0
NotificationCenter.default.removeObserver(self, name: UITextView.textDidChangeNotification, object: nil)
return
}
guard let keyboardValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue,
let keyboardAnimationDuration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Float,
let firstResponder = (UIResponder.firstResponder as? UIView)
else { return }
let firstResponderFrame: CGRect
if let textView = firstResponder as? UITextView, let caretPosition = textView.selectedTextRange?.start {
NotificationCenter.default.addObserver(self,
selector: #selector(adjustForTextViewCaret(_:)),
name: UITextView.textDidChangeNotification,
object: textView)
firstResponderFrame = view.convert(textView.caretRect(for: caretPosition), from: firstResponder)
} else {
firstResponderFrame = view.convert(firstResponder.frame, from: firstResponder.superview)
}
keyboardFrame = view.convert(keyboardValue.cgRectValue, from: view.window)
let offset = (firstResponderFrame.maxY - keyboardFrame!.minY) + margin - view.frame.origin.y
if keyboardAnimationDuration > 0 && offset > 0 {
// Animated implicitly with the keyboard.
view.frame.origin.y = -offset
} else {
// Wrap inside an animation block as there is no keyboard animation (jumping between text fields).
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut, animations: {
if offset > 0 {
self.view.frame.origin.y = -offset
}
})
}
}
@objc
private func adjustForTextViewCaret(_ notification: NSNotification) {
guard let textView = notification.object as? UITextView,
let keyboardFrame = keyboardFrame,
let caretPosition = textView.selectedTextRange?.start else {
return
}
let caretFrame = view.convert(textView.caretRect(for: caretPosition), from: textView)
let offset = (caretFrame.maxY - keyboardFrame.minY) + margin
UIView.animate(withDuration: 0.25, delay: 0, options: .curveEaseOut, animations: {
if offset > 0 {
self.view.frame.origin.y = -offset
}
})
}
}
private extension UIResponder {
private static weak var _firstResponder: UIResponder?
static var firstResponder: UIResponder? {
_firstResponder = nil
UIApplication.shared.sendAction(#selector(UIResponder.findFirstResponder(_:)), to: nil, from: nil, for: nil)
return _firstResponder
}
@objc
private func findFirstResponder(_ sender: Any) {
UIResponder._firstResponder = self
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment