I have created a chat UI in which I have added a constraint
for the tableView
to the bottom of the screen. I am changing the constraint value by ad
Make an outlet to your constraint to your ViewController
. I'll refer to it as yourConstraint
for now. Then add code to discover when the keyboard is being shown and when it is being dismissed. There you change the constant
of the constraint accordingly. This allows you to keep using constraints.
In viewWillAppear
:
NotificationCenter.default.addObserver(self, selector: #selector(YourViewController.keyboardWillShow), name: NSNotification.Name.UIKeyboardWillShow, object: nil) // <=== Replace YourViewController with name of your view controller
NotificationCenter.default.addObserver(self, selector: #selector(YourViewController.keyboardWillHide), name: NSNotification.Name.UIKeyboardWillHide, object: nil) // <=== Replace YourViewController with name of your view controller
In viewWillDisappear
:
NotificationCenter.default.removeObserver(self)
In your UIViewController
@objc private func keyboardWillShow(notification: Notification) {
guard let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue else {
yourConstraint.isActive = false // <===
view.layoutIfNeeded()
return
}
let duration: TimeInterval = ((notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue) ?? 0.4
yourConstraint.constant = newValue // <===
UIView.animate(withDuration: duration) {
self.view.layoutIfNeeded()
}
}
@objc private func keyboardWillHide(notification: Notification) {
let duration: TimeInterval = ((notification.userInfo?[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue) ?? 0.4
yourConstraint.constant = oldValue
UIView.animate(withDuration: duration) {
self.view.layoutIfNeeded()
}
}
The UIView.animate
is not necessary (the inside of the block is), but it makes the transition look nice.
Try subtracting the height of the safe area's bottom inset when calculating the value for your constraint.
Here is a sample implementation which handles a UIKeyboardWillChangeFrame
notification.
@objc private func keyboardWillChange(_ notification: Notification) {
guard let userInfo = (notification as Notification).userInfo, let value = userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue else { return }
let newHeight: CGFloat
if #available(iOS 11.0, *) {
newHeight = value.cgRectValue.height - view.safeAreaInsets.bottom
} else {
newHeight = value.cgRectValue.height
}
myConstraint.value = newHeight
}
This worked for me
TL;DR: self.view.safeAreaInsets.bottom returned 0. The key was to use UIApplication.shared.keyWindow.safeAreaInsets.bottom instead [Source].
Let replyField be the UITextField of interest.
1) Add the observers in viewDidLoad().
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
2) Set global variables within the class as the constraints.
var textFieldBottomConstraintKeyboardActive : NSLayoutConstraint?
var textFieldBottomConstraintKeyboardInactive : NSLayoutConstraint?
3) Set the constraints in a function called in viewDidLoad.
let replyFieldKeyboardActiveConstraint = self.replyField.bottomAnchor.constraint(
equalTo: self.view.safeAreaLayoutGuide.bottomAnchor,
constant: -1 * Constants.DEFAULT_KEYBOARD_HEIGHT /* whatever you want, we will change with actual height later */ + UITabBar.appearance().frame.size.height
)
let replyFieldKeyboardInactiveConstraint = self.replyField.bottomAnchor.constraint(
equalTo: self.view.safeAreaLayoutGuide.bottomAnchor
)
self.textFieldBottomConstraintKeyboardActive = replyFieldKeyboardActiveConstraint
self.textFieldBottomConstraintKeyboardInactive = replyFieldKeyboardInactiveConstraint
self.textFieldBottomConstraintKeyboardActive?.isActive = false
self.textFieldBottomConstraintKeyboardInactive?.isActive = true
4) Define the keyboardWillShow and keyboardWillHide methods. They key here is how we define the heightOffset in the keyboardWillShow method.
@objc func keyboardWillShow(notification: NSNotification) {
guard let userinfo = notification.userInfo else {
return
}
guard let keyboardSize = userinfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else {
return
}
let keyboardFrame = keyboardSize.cgRectValue
self.view.layoutIfNeeded()
let heightOffset = keyboardFrame.height - UITabBar.appearance().frame.height - (UIApplication.shared.keyWindow?.safeAreaInsets.bottom ?? 0)
self.textFieldBottomConstraintKeyboardActive?.constant = -1 * heightOffset
self.textFieldBottomConstraintKeyboardActive?.isActive = true
self.textFieldBottomConstraintKeyboardInactive?.isActive = false
self.view.setNeedsLayout()
}
@objc func keyboardWillHide(notification: NSNotification) {
self.textFieldBottomConstraintKeyboardActive?.isActive = false
self.textFieldBottomConstraintKeyboardInactive?.isActive = true
}
My issue was that my view was in a child view controller. Converting the CGRect did the trick.
@objc private func keyboardWillChangeFrame(notification: NSNotification) {
guard let userInfo = notification.userInfo, let endFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue, let duration: TimeInterval = (userInfo[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber)?.doubleValue else {
return
}
let convertedFrame = view.convert(endFrame, from: nil)
let endFrameY = endFrame.origin.y
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 {
inputContainerViewBottomLayoutConstraint.constant = 0.0
} else {
var newHeight = view.bounds.size.height - convertedFrame.origin.y
if #available(iOS 11.0, *) {
newHeight = newHeight - view.safeAreaInsets.bottom
}
inputContainerViewBottomLayoutConstraint.constant = newHeight
}
UIView.animate(withDuration: duration, delay: TimeInterval(0), options: animationCurve, animations: {
self.view.layoutIfNeeded()
},completion: { _ in
self.scrollView.scrollToBottom()
})
}
Swift 4/5 iPhone X I had to tweak Nathan's answer a little to get it to work. 100% this one does.
Note: Ensure you have control dragged from your bottom constraint for your textView/view from your storyboard to the bottom of your safe area in your ViewController, and that you have also control dragged and created an outlet in your target view controller's Project Navigator. I have named it bottomConstraint in my example. My text input field is wrapped in a view (MessageInputContainerView) to allow for additional send button alignment etc.
Here is the code:
@objc private func keyboardWillChange(_ notification: Notification) {
guard let userInfo = (notification as Notification).userInfo, let value = userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue else {
return
}
if ((userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue) != nil {
let isKeyboardShowing = notification.name == NSNotification.Name.UIKeyboardWillShow
var newHeight: CGFloat
if isKeyboardShowing {
if #available(iOS 11.0, *) {
newHeight = value.cgRectValue.height - view.safeAreaInsets.bottom
bottomConstraint.constant = newHeight
}
}
else {
newHeight = value.cgRectValue.height
bottomConstraint?.constant = view.safeAreaInsets.bottom + messageInputContainerView.bounds.height
}
}
}
The keyboard value reported by UIKeyboardFrameBeginUserInfoKey is different in these two cases in iPhone X:
To get the final height of the keyboard of the keyboard (including the safe area insets) use UIKeyboardFrameEndUserInfoKey.
In iOS 11 (iPhone X particularly), you may consider subtracting the safe area bottom insets.
NSValue *keyboardEndFrameValue = notification.userInfo[UIKeyboardFrameEndUserInfoKey];
if (keyboardEndFrameValue != nil) {
CGRect keyboardSize = [keyboardEndFrameValue CGRectValue];
_keyboardHeight = keyboardSize.size.height;
if (@available(iOS 11.0, *)) {
CGFloat bottomSafeAreaInset = self.view.safeAreaInsets.bottom;
_keyboardHeight -= bottomSafeAreaInset;
} else {
// Fallback on earlier versions
}
}