I have a pageviewcontroller, which scrolls horizontally, displaying questions and answers.
The contentView for the pageviewcontroller is populated with a scrollview, and inside of this is some stack views, one of which has a textView, for user entry.
When the user taps on the textView, the keyboard pops up, sometimes covering up the textView. When I try to scroll vertically, as I would expect scrollview to allow, the scrollview doesn't respond, I can only scroll horizontally to the next page view.
The problem is that when the keyboard pops up after the user taps in the textview, the textview may be hidden underneath the keyboard, so the user can't see what he's typing. I'd like to have the view scroll upward when he keyboard is tapped, so the user can see what is being typed.
That's called "Keyboard managing"
You have to:
1) Set observer for keyboard appear and keyboard disappear action
2) Update bottom constraint, when keyboard appears.
3) Update it again, when keyboard disappears.
4) Delete observers, when view will disappear.
Step by step instructions, for example here: Move view with keyboard using Swift
But my recommendation is to update constraints, not origins, as in instruction. Or, maybe, you can set an offset for content
you should set an bottom constraint into your stack view
and then controll drag that in your class like this :
class yourClassViewController: UIViewController {
// MARK: Properties
#IBOutlet weak var bottomConstraint: NSLayoutConstraint!
then in your viewDidLoad method write like this:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self,
selector: #selector(self.keyboardNotification(notification:)),
name: NSNotification.Name.UIKeyboardWillChangeFrame,
object: nil)
// Do any additional setup after loading the view.
}
and below that:
deinit {
NotificationCenter.default.removeObserver(self)
}
#objc func keyboardNotification(notification: NSNotification) {
if let userInfo = notification.userInfo {
let endFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
let endFrameY = endFrame?.origin.y ?? 0
let duration:TimeInterval = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0
let animationCurveRawNSN = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber
let animationCurveRaw = animationCurveRawNSN?.uintValue ?? UIViewAnimationOptions.curveEaseInOut.rawValue
let animationCurve:UIViewAnimationOptions = UIViewAnimationOptions(rawValue: animationCurveRaw)
if endFrameY >= UIScreen.main.bounds.size.height {
self.bottomConstraint?.constant = 0.0
} else {
self.bottomConstraint?.constant = endFrame?.size.height ?? 0.0
}
UIView.animate(withDuration: duration,
delay: TimeInterval(0),
options: animationCurve,
animations: { self.view.layoutIfNeeded() },
completion: nil)
}
}
for disappear keyboard in touchsBegan method you should write like this:
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.view.endEditing(true)
}
Use notification observers to handle keyboard show and hide events:
// keyboard will show
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShowAction(_:)), name: .UIKeyboardWillShow, object: nil)
// keyboard will hide
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHideAction(_:)), name: .UIKeyboardWillHide, object: nil)
You can do something like this when the keyboard is about to appear:
// keyboard will show action
#objc private func keyboardWillShowAction(_ notification: NSNotification) {
currentScrollViewOffset = scrollView.contentOffset
let keyboardFrame = notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue
let keyboardHeight = keyboardFrame?.cgRectValue.height
let padding: CGFloat = 32
// if text field view would be obstructed by keyboard, apply minimum-needed scroll view offset
if activeTextFieldMaxYOnScreen! > (UIScreen.main.bounds.height - keyboardHeight! - padding) {
let temporaryOffset = activeTextFieldMaxYOnScreen! - (UIScreen.main.bounds.height - (keyboardHeight! + padding))
scrollView.setContentOffset(CGPoint(x: 0, y: currentScrollViewOffset.y + temporaryOffset), animated: true)
}
scrollView.addGestureRecognizer(tapToDismissKeyboard)
scrollView.isScrollEnabled = false
}
And then revert when the keyboard is dismissed:
// keyboard will hide action
#objc private func keyboardWillHideAction(_ notification: NSNotification) {
scrollView.setContentOffset(currentScrollViewOffset, animated: true)
scrollView.removeGestureRecognizer(tapToDismissKeyboard)
scrollView.isScrollEnabled = true
}
It's also good practice to remove observers in the deinit method:
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillShow, object: nil)
NotificationCenter.default.removeObserver(self, name: .UIKeyboardWillHide, object: nil)
Note that currentScrollViewOffset and activeTextFieldMaxYOnScreen are instance properties of the view controller. To get the activeTextFieldMaxYOnScreen value, you can get it from the text field delegate:
// text field should begin editing
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
// convert text field line's max-y to perceived max-y on the screen
let line = textField.viewWithTag(123)
activeTextFieldMaxYOnScreen = (line?.convert((line?.bounds.origin)!, to: nil).y)! + (line?.frame.height)!
return true
}
Related
I have a text input field at the bottom of my view, which I'm trying to animate up and down to stay on top of the keyboard.
func setupKeyboardObservers() {
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardWillChangeFrame), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardDidShow), name: UIResponder.keyboardDidShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(handleKeyboardWillChangeFrame), name: UIResponder.keyboardWillHideNotification, object: nil)
}
#objc func handleKeyboardWillChangeFrame(notification: NSNotification) {
let keyboardFrame = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
let keyboardDuration = (notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? Double)
print(keyboardFrame)
orderDetailView?.textInputViewBottomAnchor?.constant = -keyboardFrame!.height
UIView.animate(withDuration: keyboardDuration!) {
self.view.layoutIfNeeded()
}
}
OrderDetailView is the view for the viewcontroller.
The textinputview is the part that animates, and it works correctly when the keyboard first shows up, but does not animate back when I send a message and the keyboard resigns first responder, nor if I resignfirstresponder by clicking outside the keyboard.
When I print the cgrect value from keyboardFrameEndUserInfoKey, it returns the same frame value as when the keyboard is present (instead of 0).
This only seems to work properly when I drag down the keyboard from the view.
Thanks for your help.
In your case the height is still non-zero when keyboard hides which I assume is your issue. You need to convert keyboard frame to your view coordinate system and setup constraints according to that. Check the following:
#objc private func onKeyboardChange(notification: NSNotification) {
guard let info = notification.userInfo else { return }
guard let value: NSValue = info[UIKeyboardFrameEndUserInfoKey] as? NSValue else { return }
let newFrame = value.cgRectValue
if let durationNumber = info[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber, let keyboardCurveNumber = info[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber {
let duration = durationNumber.doubleValue
let keyboardCurve = keyboardCurveNumber.uintValue
UIView.animate(withDuration: duration, delay: 0, options: UIViewAnimationOptions(rawValue: keyboardCurve), animations: {
self.updateFromKeyboardChangeToFrame(newFrame)
}, completion: { _ in
// After animation
})
} else {
self.updateFromKeyboardChangeToFrame(newFrame)
}
}
private func updateFromKeyboardChangeToFrame(_ keyboardFrame: CGRect) {
let view: UIView! // Whatever view that uses bottom constraint
let bottomConstraint: NSLayoutConstraint! // Your bottom constraint
let constant = view.bounds.height-max(0, view.convert(keyboardFrame, from: nil).origin.y)
bottomConstraint.constant = max(0, constant)
view.layoutIfNeeded()
}
In your case you seem to use
let view = self.view
let bottomConstraint = orderDetailView?.textInputViewBottomAnchor
and it depends on how you define your constraint but it seems you will need to use negative values as bottomConstraint.constant = -max(0, constant).
my needs:
to move the UIView up base on whether textField covered by keyboard and how much it covered.
Don't want to use scrollView to rebuild it.
to achieve 1. I have to know the coordinate Y of the textField I chose. Lots of
answers using NSNotificationCenter to build a listener. There is what i don't understand, why set nil to object? isn't the object who trigger the function below? I try to get the position of the object but failed. Because I set the nil to object. try to set textField to object still not working.
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.keyboardWillShow), name: UIKeyboardWillShowNotification, object: UITextField())
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(ViewController.keyboardWillHide), name: UIKeyboardWillHideNotification, object: UITextField())
How to get the position of the chosen textField? if not declare something like this:
#IBOutlet var textField: UITextField!
Is there hidden information inside notification? maybe in the userInfo? cause the keyboard's height is in it.
let keyboardFrame = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).CGRectValue()
Here is my code:
import UIKit
class VC_Base: UIViewController, UITextFieldDelegate{
var keyBoardShow:Bool = false
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(VC_Base.keyboardWillShow), name: UIKeyboardWillShowNotification, object: self)
NSNotificationCenter.defaultCenter().addObserver(self, selector: #selector(VC_Base.keyboardWillHide), name: UIKeyboardWillHideNotification, object: self)
// Do any additional setup after loading the view.
}
func keyboardWillShow(notification: NSNotification) {
guard keyBoardShow else{
adjustingHightLight(true, notification: notification)
keyBoardShow = true
return
}
keyBoardShow = true
}
func keyboardWillHide(notification: NSNotification) {
adjustingHightLight(false, notification: notification)
keyBoardShow = false
}
func adjustingHightLight(show: Bool, notification: NSNotification) {
var userInfo = notification.userInfo!
if let object = notification.object as! UITextField!{
print("there is object")
let textFieldCoverByKeyBoard = self.view.frame.maxY - object.frame.maxY
let keyboardFrame: CGRect = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).CGRectValue()
let differenceOfTextFieldAndKeyBoard = textFieldCoverByKeyBoard - keyboardFrame.height
let animationDuration = userInfo[UIKeyboardAnimationDurationUserInfoKey] as! NSTimeInterval
let changeInHeight = differenceOfTextFieldAndKeyBoard * (show ? 1 : -1)
if differenceOfTextFieldAndKeyBoard < 5{
UIView.animateWithDuration(animationDuration, delay: 0, usingSpringWithDamping: 0, initialSpringVelocity: 0, options: UIViewAnimationOptions.CurveLinear, animations: { () -> Void in
self.view.frame.offsetInPlace(dx: 0, dy: changeInHeight)}, completion: nil)
}
}
}
func textFieldShouldReturn(textField: UITextField) -> Bool {
self.view.endEditing(true)
return true
}
override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
self.view.endEditing(true)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
texting is the basic function it should achieve several things below:
1.whether the keyboard cover textField?
if true, how much did it cover and move the UIView upward the value so the keyboard won't cover it.
if false, do nothing and show keyboard.
2.if I already select one textField and then chose another. it won't move up again (which will happened in some answers).
But seems there are not much answer fulfill it except using scrollView(which I don't want to rebuild...)
please help!!THX.
I'm working on an iOS app and currently all my elements are in a scroll view and when the keyboard is present I move the view up 250 pts. This solved my problem but the keyboard is always a different size per device.
How could I detect how far from the bottom of the screen my text field is and how tall the keyboard is?
You should observe the notification for showing and hiding the keyboard. And after that you can get the exact keyboard size and either shift or change the content insets of your scroll view. Here's a sample code:
extension UIViewController {
func registerForKeyboardDidShowNotification(scrollView: UIScrollView, usingBlock block: (NSNotification -> Void)? = nil) {
NSNotificationCenter.defaultCenter().addObserverForName(UIKeyboardDidShowNotification, object: nil, queue: nil, usingBlock: { (notification) -> Void in
let userInfo = notification.userInfo!
let keyboardSize = userInfo[UIKeyboardFrameBeginUserInfoKey]?.CGRectValue.size
let contentInsets = UIEdgeInsetsMake(scrollView.contentInset.top, scrollView.contentInset.left, keyboardSize!.height, scrollView.contentInset.right)
scrollView.scrollEnabled = true
scrollView.setContentInsetAndScrollIndicatorInsets(contentInsets)
block?(notification)
})
}
func registerForKeyboardWillHideNotification(scrollView: UIScrollView, usingBlock block: (NSNotification -> Void)? = nil) {
NSNotificationCenter.defaultCenter().addObserverForName(UIKeyboardWillHideNotification, object: nil, queue: nil, usingBlock: { (notification) -> Void in
let contentInsets = UIEdgeInsetsMake(scrollView.contentInset.top, scrollView.contentInset.left, 0, scrollView.contentInset.right)
scrollView.setContentInsetAndScrollIndicatorInsets(contentInsets)
scrollView.scrollEnabled = false
block?(notification)
})
}
}
extension UIScrollView {
func setContentInsetAndScrollIndicatorInsets(edgeInsets: UIEdgeInsets) {
self.contentInset = edgeInsets
self.scrollIndicatorInsets = edgeInsets
}
}
And in your UIViewController in which you want to shift the scrollview, just add next lines under the viewDidLoad() function
override func viewDidLoad() {
super.viewDidLoad()
registerForKeyboardDidShowNotification(scrollView)
registerForKeyboardWillHideNotification(scrollView)
}
I'm currently work on this and found a solution. First you need to add a notification to the view controller to identify whether the keyboard is on or not. For that you need to register this notification in viewDidLoad().
override func viewDidLoad() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardDidShow:", name: UIKeyboardWillChangeFrameNotification, object: nil)
}
And also don't forget to remove this notification, when the view controller removed from the view. So remove this notification on viewDidDisappear().
override func viewDidDisappear(animated: Bool) {
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillChangeFrameNotification, object: nil)
}
And the final thing is to manage the scroll view when the keyboard is on or off. So first you need to identify the keyboard height.Then for pretty smooth animation, you can use keyboard animation mood and duration time to animate your scroll view.
func keyboardDidShow(notification: NSNotification) {
if let userInfo = notification.userInfo {
let endFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.CGRectValue()
let duration:NSTimeInterval = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue ?? 0
let animationCurveRawNSN = userInfo[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber
let animationCurveRaw = animationCurveRawNSN?.unsignedLongValue ?? UIViewAnimationOptions.CurveEaseInOut.rawValue
let animationCurve:UIViewAnimationOptions = UIViewAnimationOptions(rawValue: animationCurveRaw)
if endFrame?.origin.y >= UIScreen.mainScreen().bounds.size.height {
//isKeyboardActive = false
UIView.animateWithDuration(duration,
delay: NSTimeInterval(0),
options: animationCurve,
animations: {
// move scroll view height to 0.0
},
completion: { _ in
})
} else {
//isKeyboardActive = true
UIView.animateWithDuration(duration,
delay: NSTimeInterval(0),
options: animationCurve,
animations: {
// move scroll view height to endFrame?.size.height ?? 0.0
},
completion: { _ in
})
}
}
}
#noir_eagle answer seems right.
But there may be is a simpler solution. Maybe you could try using IQKeyboardManager. It allows you to handle these keyboard things in a simple and seamless way.
I think you really should, at least, spend few minutes looking at it.
i have developed on im app. when the user taps the text field the keyboard pops up and the table view containing the messages moves up but the top of the table view is un viewable as it is off the screen. it is also having the undesired effect of turning the screen black when the user moves the keyboard suggestions up or down
override func viewWillAppear(animated:Bool) {
super.viewWillAppear(animated)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillShow:"), name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillHide:"), name: UIKeyboardWillHideNotification, object: nil)
}
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) {
let movement = (up ? -kbHeight : kbHeight)
UIView.animateWithDuration(0.2, animations: {
self.view.frame = CGRectOffset(self.view.frame, 0, movement)
})
}
How can i control the display of the tableview, text field and keyboard?
Edit
desired behaviour.
from the start
i would like the view keyboard to be collapsed (working fine)
table view showing the messages (working fine)
and ta text field at the bottom of the screen (working fine)
when Text field is tapped
keyboard pops up (working)
text field moves to top of keyboard (working)
table view displays in remaining space above text field (not working, it is above textfield but half is spilling of the screen to the top)
other bug
the screen is going black when show/hide the autocomplete words on top of the keyboard
I found the solution and it worked for me.
Please check in your project. (Eendje's solution)
Resize the screen when keyboard appears
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: NSTimeInterval = (info[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
UIView.animateWithDuration(duration) { self.view.layoutIfNeeded() }
}
func keyboardWillHide(sender: NSNotification) {
let info = sender.userInfo!
let duration: NSTimeInterval = (info[UIKeyboardAnimationDurationUserInfoKey] as! NSNumber).doubleValue
bottomConstraint.constant = 0
UIView.animateWithDuration(duration) { self.view.layoutIfNeeded() }
}
If you have trouble creating the bottom constraint:
In storyboard
Select your search bar.
At the corner in the lower right you'll see 3 icons. Click the middle one looking like |-[]-|.
At the top of that popup, there are 4 boxes. Enter 0 at the one for the bottom.
Constraint created!
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!
var 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.
I used constraint solution to fix my problem.
try this for keyboard.
-viewDidLoad()
let tap: UITapGestureRecognizer = UITapGestureRecognizer(target: self, action: "dismissKeyboard")
view.addGestureRecognizer(tap)
and
func dismissKeyboard(){
view.endEditing(true)
}
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?
2020 UPDATE
Correctly using a constraint...
There is only one way to properly handle this mess in iOS.
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.
So "which view should you resize?"
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 ...
Use KUIViewController anywhere a keyboard might appear.
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 )
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
Select your search bar.
At the corner in the lower right you'll see 3 icons. Click the middle one looking like |-[]-|.
At the top of that popup, there are 4 boxes. Enter 0 at the one for the bottom.
Constraint created!
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
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)
}
}
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
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: