I'm trying to update the constraints of my view when the keyboard appears and disappears
I have notifications for keyboardWillShow and keyboardWillHide and corresponding functions to call them:
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name:UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil)
#objc private func keyboardWillShow(notification: NSNotification) {
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
let keyboardFrameEnd = keyboardFrame.cgRectValue
let convertedKeyboardFrameEnd = self.view.convert(keyboardFrameEnd, from: nil)
let intersection = convertedKeyboardFrameEnd.intersection(listView.frame)
if intersection.size.height > 0 {
self.listView.snp.makeConstraints { make in
make.top.leading.trailing.equalTo(0)
make.bottom.equalTo(-intersection.height)
}
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
}
}
}
#objc private func keyboardWillHide() {
self.listView.snp.makeConstraints { make in
make.edges.equalTo(0)
make.bottom.equalTo(0)
}
self.listView.setNeedsUpdateConstraints()
self.listView.updateConstraints()
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
}
It seems that keyboardWillShow gets called and updates the size of my listView to be the visible size outside of the keyboard. However, keyboardWillHide gets called and it doesn't seem to update the bottom back to have value 0 so I end up with a case where my view is not resized back to its original size. Any ideas what might be wrong here?
Related
so I have a small app where I choose an image, get it into an image view
and I have 2 textfield to insert a funny phrase
(Meme editor app)
my problem is since the bottom textfield is covered when the keyboard is shown I had to shift the view upwards every time the keyboard is shown for the bottom texfield and I succeed in doing that, what goes wrong is that every time I re-tap the beginning or the end of an existing text in the text filed the view shifts up again in undesirable behavior
here is a small GIF that shows what happens exactly
here is my code so far:
Function to get Kyboard height:
func getKeyboardHeight(_ notification:Notification) -> CGFloat {
let userInfo = notification.userInfo
let keyboardSize = userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! NSValue // of CGRect
return keyboardSize.cgRectValue.height
}
Function to shift the view up in the condition that bottom textfield is what the user taps
#objc func keyboardWillShow(_ notification:Notification) {
if bottomTextField.isFirstResponder{
view.frame.origin.y -= getKeyboardHeight(notification)
}
}
Function to return the view to its normal position when the user finishes editing the bottom text field
#objc func keyboardWillHide(_ notification:Notification) {
view.frame.origin.y = 0
}
Functions to add and remove observers of keyboard notifications
func subscribeToKeyboardNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
}
func subscribekeyboardWillHide() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}
func unsubscribeFromKeyboardNotifications() {
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
}
func unsubscribekeyboardWillHide() {
NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
}
and where I call them
override func viewWillAppear(_ animated: Bool) {
super .viewWillAppear(true)
cameraButton.isEnabled = UIImagePickerController.isSourceTypeAvailable(.camera)
subscribeToKeyboardNotifications()
subscribekeyboardWillHide()
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
unsubscribeFromKeyboardNotifications()
unsubscribekeyboardWillHide()
}
if you could be kind to provide a simple explanation for your solution I would appreciate it
This is what I do while managing keyboard. It causes no problems.
Declare in your UIViewController class
private let notificationCenter = NotificationCenter.default
then in
override func viewDidLoad() {
super.viewDidLoad()
notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil)
notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
}
This is the adjust for keyboard function
#objc private func adjustForKeyboard(notification: Notification) {
guard let userInfo = notification.userInfo else { return }
guard let keyboardScreenEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { return }
let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window)
if notification.name == UIResponder.keyboardWillHideNotification {
scrollView.contentInset = UIEdgeInsets.zero
} else {
scrollView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardViewEndFrame.height, right: 0)
}
scrollView.scrollIndicatorInsets = scrollView.contentInset
}
Deinit here -
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(true)
notificationCenter.removeObserver(self, name: UIResponder.keyboardWillHideNotification, object: nil)
notificationCenter.removeObserver(self, name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
}
Every time you change the cursor location, UIResponder.keyboardWillShowNotification notification triggered, thats why every time you tap the textfield it moves up one keyboard height more.
You can use UIResponder.keyboardDidShowNotification notification instead of UIResponder.keyboardWillShowNotification. This one is not triggered when cursor location changes.
In my application I need to get keyboard size for after move the others components to correctly position, now i'm using that code to get height
let info:NSDictionary = aNotification.userInfo! as NSDictionary
let kbSize:CGSize = (info.object(forKey: UIResponder.keyboardFrameEndUserInfoKey)! as AnyObject).cgRectValue.size
print(kbSize.height)
In the first time when keyboard is open the result is 260.0, but this value is size default, without the suggestions/passwords bar height, when I click out and click in the input again the result is 304.0 the value I want...
How I can get 304 since the first click in the input?
You should be listening for changes in the keyboard's size and adjusting the rest of your content that way, since, as you found out, iOS keyboards can change size. You should subscribe to UIResponder.keyboardWillChangeFrameNotification and/or UIResponder.keyboardDidChangeFrameNotification which are notifications which trigger when the keyboard's frame will, or did, change. Here's an example of it in use:
// Somewhere in set up code
NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidChangeFrame),
name: UIResponder.keyboardDidChangeFrameNotification, object: nil)
// Function elsewhere in your class
#objc func keyboardDidChangeFrame(_ notification: Notification) {
guard let kbSize = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey]
as? CGRect else {
return;
}
// Use kbRect as you initially did
}
Note that you can use either the Will or the Did notifications depending on how you want the layout change to look. You can also query both UIResponder.keyboardFrameBeginUserInfoKey UIResponder.keyboardFrameEndUserInfoKey to get the keyboard frame before and after the size change, which may be useful if you want to animate your layout along with the keyboard.
You can try this;
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChange(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChange(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillChange(notification:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil)
}
deinit {
NotificationCenter.default.removeObserver(UIResponder.keyboardWillShowNotification)
NotificationCenter.default.removeObserver(UIResponder.keyboardWillHideNotification)
NotificationCenter.default.removeObserver(UIResponder.keyboardWillChangeFrameNotification)
}
#objc func keyboardWillChange(notification: Notification) {
guard let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else { return }
if notification.name == UIResponder.keyboardWillChangeFrameNotification || notification.name == UIResponder.keyboardWillShowNotification {
view.frame.origin.y = -keyboardSize.height
} else {
view.frame.origin.y = 0
}
}
}
HERE is the Github gist
In my app, I wanted a notification for UIResponder.keyboardWillShowNotification in order to update the y position of my text field. It was working prior to iOS 12; now, it is not called in one of my view controllers (it works for other ones).
Here is my code to do this:
#objc func keyboardWillShow(_ notification: Notification) {
print("keyboard will show 2")
guard let frameValue: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else {
return
}
let keyboardFrame = frameValue.cgRectValue
UIView.animate(withDuration: animationTime) {
self.addViewBottomConstraint.constant = keyboardFrame.size.height
self.view.layoutIfNeeded()
print("Bottom contraint height = \(self.addViewBottomConstraint.constant)")
}
}
#objc func keyboardWillHide(_ notification: Notification) {
UIView.animate(withDuration: animationTime) {
self.addViewBottomConstraint.constant = 0
self.view.layoutIfNeeded()
}
}
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil)
}
Here, "keyboard will show 2" is not printed, but it is printed for other view controllers with the same notifications. Is there anything new in iOS 12 that caused this? Otherwise, is there a particular reason why it is not being called?
This can be related to simulator setup, see menu "Hardware > Keyboard > Connect Keyboard Hardware". If this option is ON, you will get UIKeyboardWillHideNotification, but never UIKeyboardWillShowNotification.
I have a view which is resize a constraint when the keyboard appear. So I have notifications when the keyboard appears and disappears.
The above behaviour occurs when the keyboard is already shown and I rotate the screen. Then the next actions occurs:
UIKeyboardWillHideNotification called
UIKeyboardWillShowNotification called (with the old height of the keyboard)
UIKeyboardWillShowNotification called (with the new height of the keyboard)
So, the updateView function receives first one height and later a different height. This results in a weird movements of the view adjusting twice the value.
override func viewDidLoad() {
super.viewDidLoad()
// Creates notification when keyboard appears and disappears
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillShow:"), name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillHide:"), name: UIKeyboardWillHideNotification, object: nil)
}
deinit {
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillHideNotification, object: nil)
}
func keyboardWillShow(notification: NSNotification) {
self.adjustingHeight(true, notification: notification)
}
func keyboardWillHide(notification: NSNotification) {
self.adjustingHeight(false, notification: notification)
}
private func adjustingHeight(show: Bool, notification: NSNotification) {
// Gets notification information in an dictionary
var userInfo = notification.userInfo!
// From information dictionary gets keyboard’s size
let keyboardFrame:CGRect = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).CGRectValue()
// Gets the time required for keyboard pop up animation
let animationDurarion = userInfo[UIKeyboardAnimationDurationUserInfoKey] as! NSTimeInterval
// Animation moving constraint at same speed of moving keyboard & change bottom constraint accordingly.
if show {
self.bottomConstraint.constant = (CGRectGetHeight(keyboardFrame) + self.bottomConstraintConstantDefault / 2)
} else {
self.bottomConstraint.constant = self.bottomConstraintConstantDefault
}
UIView.animateWithDuration(animationDurarion) {
self.view.layoutIfNeeded()
}
self.hideLogoIfSmall()
}
Finally I found the solution. When I get keyboardFrame, I was using UIKeyboardFrameBeginUserInfoKey which returns the frame of the keyboard before the animation begins. The correct way of do that is with UIKeyboardFrameEndUserInfoKeywhich returns the frame of the keyboard after the animation has completed.
let keyboardFrame: CGRect = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).CGRectValue()
I create notifications when the keyboard appears and disappears.
override func viewDidLoad() {
super.viewDidLoad()
// Creates notification when keyboard appears and disappears
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillShow:"), name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillHide:"), name: UIKeyboardWillHideNotification, object: nil)
}
override func viewWillDisappear(animated: Bool) {
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().removeObserver(self, name: UIKeyboardWillHideNotification, object: nil)
}
func keyboardWillShow(notification: NSNotification) {
self.adjustingHeight(true, notification: notification)
}
func keyboardWillHide(notification: NSNotification) {
self.adjustingHeight(false, notification: notification)
}
private func adjustingHeight(show: Bool, notification: NSNotification) {
// Gets notification information in an dictionary
var userInfo = notification.userInfo!
// From information dictionary gets keyboard’s size
let keyboardFrame:CGRect = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).CGRectValue()
// Gets the time required for keyboard pop up animation
let animationDurarion = userInfo[UIKeyboardAnimationDurationUserInfoKey] as! NSTimeInterval
// Animation moving constraint at same speed of moving keyboard & change bottom constraint accordingly.
if !(show && self.bottomConstraint.constant != self.bottomConstraintConstantDefault) {
if show {
self.bottomConstraint.constant = (CGRectGetHeight(keyboardFrame) + self.bottomConstraintConstantDefault / 2)
} else {
self.bottomConstraint.constant = self.bottomConstraintConstantDefault
}
UIView.animateWithDuration(animationDurarion) {
self.view.layoutIfNeeded()
}
}
self.hideLogoIfSmall()
}
The strange behaviour occurs when the keyboard is already shown and I rotate the screen. Then the next actions occurs:
UIKeyboardWillHideNotification called
UIKeyboardWillShowNotification called (with the old height of the keyboard)
UIKeyboardWillShowNotification called (with the new height of the keyboard)
The result of this is that my view is not updated properly because the first time UIKeyboardWillShowNotification is called I receive a different height of the keyboard than the second time.
Finally I found the solution. When I get keyboardFrame, I was using UIKeyboardFrameBeginUserInfoKey which returns the frame of the keyboard before the animation begins. The correct way of do that is with UIKeyboardFrameEndUserInfoKeywhich returns the frame of the keyboard after the animation has completed.
let keyboardFrame: CGRect = (userInfo[UIKeyboardFrameEndUserInfoKey] as! NSValue).CGRectValue()
Now UIKeyboardWillShowNotification is called twice but returns the same height of the keyboard in both calls.