Created
May 10, 2022 08:37
-
-
Save vinczebalazs/48e62aded1c88bbcda47ef80d4bba407 to your computer and use it in GitHub Desktop.
A simple UIViewController subclass that avoids the keyboard automatically.
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 | |
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