Reputation: 289
I am building a chat app. I have to move a textfield when keyboard appears. I am doing this with the following code:
func keyboardWillShow(notification: NSNotification) {
if let userInfo = notification.userInfo {
if let keyboardSize = (userInfo[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.CGRectValue() {
kbHeight = keyboardSize.height
self.animateTextField(true)
}
}
}
func keyboardWillHide(notification: NSNotification) {
self.animateTextField(false)
}
func animateTextField(up: Bool) {
var movement = (up ? -kbHeight : kbHeight)
UIView.animateWithDuration(0.3, animations: {
self.view.frame = CGRectOffset(self.view.frame, 0, movement)
})
}
But when I use this code, the first messages doesn't show. I guess I have to resize the tableview.
Here are screenshots Before and After the keyboard appears:
I am using auto layout.
How can I resolve this problem?
Upvotes: 27
Views: 22679
Reputation: 12631
Correctly using a constraint...
Paste KUIViewController
below in to your project,
Create a constraint which is very simply to the "bottom of your content".
Drag that constraint to bottomConstraintForKeyboard
KUIViewController will automatically and correctly resize your content view at all times.
Absolutely everything is totally automatic.
All Apple behaviors are handled correctly in the standard way, such as dismissing by taps, etc etc.
You are 100% completely done.
You can not use .view
...
Because ... you cannot resize the .view
in iOS!!!!!! Doh!
Simply make a UIView named "holder". It sits inside .view
.
Put everything of yours inside "holder".
Holder will of course have four simple constraints top/bottom/left/right to .view
.
That bottom constraint to "holder" is indeed bottomConstraintForKeyboard
.
You're done.
Send a bill to the cliient and go drinking.
There is nothing more to do.
class KUIViewController: UIViewController {
// KBaseVC is the KEYBOARD variant BaseVC. more on this later
@IBOutlet var bottomConstraintForKeyboard: NSLayoutConstraint!
@objc func keyboardWillShow(sender: NSNotification) {
let i = sender.userInfo!
let s: TimeInterval = (i[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
let k = (i[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue.height
bottomConstraintForKeyboard.constant = k
// Note. that is the correct, actual value. Some prefer to use:
// bottomConstraintForKeyboard.constant = k - bottomLayoutGuide.length
UIView.animate(withDuration: s) { self.view.layoutIfNeeded() }
}
@objc func keyboardWillHide(sender: NSNotification) {
let info = sender.userInfo!
let s: TimeInterval = (info[UIResponder.keyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
bottomConstraintForKeyboard.constant = 0
UIView.animate(withDuration: s) { self.view.layoutIfNeeded() }
}
@objc func clearKeyboard() {
view.endEditing(true)
// (subtle iOS bug/problem in obscure cases: see note below
// you may prefer to add a short delay here)
}
func keyboardNotifications() {
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillShow),
name: UIResponder.keyboardWillShowNotification,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillHide),
name: UIResponder.keyboardWillHideNotification,
object: nil)
}
override func viewDidLoad() {
super.viewDidLoad()
keyboardNotifications()
let t = UITapGestureRecognizer(target: self, action: #selector(clearKeyboard))
view.addGestureRecognizer(t)
t.cancelsTouchesInView = false
}
}
Simply ...
class AddCustomer: KUIViewController, SomeProtocol {
class EnterPost: KUIViewController {
class EditPurchase: KUIViewController {
On those screens absolutely everything is now completely automatic regarding the keyboard.
You're done.
Phew.
*Minor footnote - background clicks correctly dismiss the keyboard. That includes clicks which fall on your content. This is correct Apple behavior. Any unusual variation from that would take a huge amount of very anti-Apple custom programming.
*Extremely minor footnote - so, any and all buttons on the screen will work 100% correctly every time. However in the INCREDIBLY obscure case of nested (!) container views inside nested (!) scroll views with nested (!) page view containers (!!!!), you may find that a button will seemingly not work. This seems to be basically a (obscure!) problem in current iOS. If you encounter this incredibly obscure problem, fortunately the solution is simple. Looking at the function clearKeyboard()
, simply add a short delay, you're done.
@objc func clearKeyboard() {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(100)) {
self.view.endEditing(true)
}
}
(A great tip from user @wildcat12 https://stackoverflow.com/a/57698468/294884 )
Upvotes: 18
Reputation: 123
Maybe it will help somebody. You can achieve the desired behavior without using interface builder at all
First of all you will need to create a constraint and calculate safe area insets in order to support buttonless devices properly
var container: UIView!
var bottomConstraint: NSLayoutConstraint!
let safeInsets = UIApplication.shared.windows[0].safeAreaInsets
then initialize it somewhere in your code
container = UIView()
bottomConstraint = container.bottomAnchor.constraint(equalTo: view.bottomAnchor)
attach it to view and activate
view.addSubview(container)
NSLayoutConstraint.activate([
...
container.leadingAnchor.constraint(equalTo: view.leadingAnchor),
container.trailingAnchor.constraint(equalTo: view.trailingAnchor),
container.topAnchor.constraint(equalTo: view.topAnchor),
bottomConstraint,
...
])
and finally
@objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
if bottomConstraint.constant == 0 {
bottomConstraint.constant = -keyboardSize.height + safeInsets.bottom
view.layoutIfNeeded()
}
}
}
@objc func keyboardWillHide(notification: NSNotification) {
bottomConstraint.constant = 0
view.layoutIfNeeded()
}
Also if your view is something scrollable and you want to move it up with keyboard and return to initial position as the keyboard hides, you can change view's contentOffset
view.contentOffset = CGPoint(x: view.contentOffset.x, y: view.contentOffset.y + keyboardSize.height - safeInsets.bottom)
for scrolling up, and
view.contentOffset = CGPoint(x: view.contentOffset.x, y: view.contentOffset.y - keyboardSize.height + safeInsets.bottom)
to move it down
Upvotes: 2
Reputation: 31
From @Fattie 's message:
A detail - (unfortunately) clicks on your content will also dismiss the keyboard. (They both get the event.) However, this is almost always correct behavior; give it a try. There is no reasonable was to avoid this, so forget about it and go with the Apple-flow.
This can be solved by implementing the following UIGestureRecognizerDelegate's method:
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
return !(touch.view?.isKind(of: UIControl.self) ?? true)
}
That way, if the user touches any UIControl
(UIButton, UITextField, etc.) the gesture recognizer won't call the clearKeyboard()
method.
For this to work, remember to subclass UIGestureRecognizerDelegate either at class definition or with an extension. Then, in viewDidLoad() you should assign the gesture recognizer delegate as self.
Ready to copy and paste code:
// 1. Subclass UIGestureRecognizerDelegate
class KUIViewController: UIViewController, UIGestureRecognizerDelegate {
@IBOutlet var bottomConstraintForKeyboard: NSLayoutConstraint!
func keyboardWillShow(sender: NSNotification) {
let i = sender.userInfo!
let k = (i[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue.height
bottomConstraintForKeyboard.constant = k - bottomLayoutGuide.length
let s: TimeInterval = (i[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
UIView.animate(withDuration: s) { self.view.layoutIfNeeded() }
}
func keyboardWillHide(sender: NSNotification) {
let info = sender.userInfo!
let s: TimeInterval = (info[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
bottomConstraintForKeyboard.constant = 0
UIView.animate(withDuration: s) { self.view.layoutIfNeeded() }
}
func keyboardNotifications() {
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillShow),
name: Notification.Name.UIKeyboardWillShow,
object: nil)
NotificationCenter.default.addObserver(self,
selector: #selector(keyboardWillHide),
name: Notification.Name.UIKeyboardWillHide,
object: nil)
}
func clearKeyboard() {
view.endEditing(true)
}
override func viewDidLoad() {
super.viewDidLoad()
keyboardNotifications()
let t = UITapGestureRecognizer(target: self, action: #selector(clearKeyboard))
view.addGestureRecognizer(t)
t.cancelsTouchesInView = false
// 2. Set the gesture recognizer's delegate as self
t.delegate = self
}
// 3. Implement this method from UIGestureRecognizerDelegate
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
return !(touch.view?.isKind(of: UIControl.self) ?? true)
}
}
Upvotes: 2
Reputation: 8883
You can create an outlet of the bottom auto layout constraint of your table view.
Then simply use this code:
func keyboardWillShow(sender: NSNotification) {
let info = sender.userInfo!
var keyboardSize = (info[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue.height
bottomConstraint.constant = keyboardSize - bottomLayoutGuide.length
let duration: TimeInterval = (info[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
UIView.animate(withDuration: duration) { self.view.layoutIfNeeded() }
}
func keyboardWillHide(sender: NSNotification) {
let info = sender.userInfo!
let duration: TimeInterval = (info[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
bottomConstraint.constant = 0
UIView.animate(withDuration: duration) { self.view.layoutIfNeeded() }
}
If you have trouble creating the bottom constraint:
In storyboard
|-[]-|
.Now you can drag it to your view controller and add it as an outlet.
Another solution is to set the tableView.contentInset.bottom
. But I haven't done that before. If you prefer that, I can try to explain it.
Using inset:
func keyboardWillShow(sender: NSNotification) {
let info = sender.userInfo!
let keyboardSize = (info[UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue.height
tableView.contentInset.bottom = keyboardSize
}
func keyboardWillHide(sender: NSNotification) {
tableView.contentInset.bottom = 0
}
You can try this code for setting the inset. I haven't tried it myself yet, but it should be something like that.
EDIT: Changed the duration with the advice of nacho4d
Upvotes: 17
Reputation: 8020
if you don't want to fight with this yourself you might find the TPKeyboardAvoiding framework useful
Simply just following the 'installation instructions' i.e. drag and drop the appropriate .h/.m files into your project and then make you ScrollView / TableView a subclass like below:
Upvotes: 1