I am using the following code to adjust UITextView when the keyboard is shown or hidden.
var backupTextRange:NSRange?
#objc func keyboardWillShow(notification: NSNotification) {
if let rectValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
let keyboardSize = rectValue.cgRectValue.size
let contentInsets: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height, right: 0)
textView.contentInset = contentInsets
textView.scrollIndicatorInsets = contentInsets
if let backupTextRange = backupTextRange {
textView.scrollRangeToVisible(backupTextRange)
NSLog("Scrolling to range \(backupTextRange)")
}
}
}
#objc func keyboardWillHide(notification: NSNotification) {
let contentInsets: UIEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
textView.contentInset = contentInsets
textView.scrollIndicatorInsets = contentInsets
backupTextRange = self.textView.selectedRange
}
So basically when I hide the keyboard to edit the font or color of selected text, I save the text range so as to scroll back when user is done choosing a font or color in another viewController (UIFontPickerController for instance). But this results in a sloppy behavior as:
Setting the contentInset of textView automatically starts a scrolling animation to the bottom of textView (not sure if this is the expected behavior),
Calling textView.scrollRangeToVisible causes the text in backupTextRange to move to top of the screen (as opposed to where it was before when it was selected). Moreover, setting this while animation is already in progress makes animation look weird.
Related
I have a chat app that displays the messages in a table view. When I invoke the keyboard, I want:
The table view to scroll to the bottom
I want there to be no overlap between any messages and the keyboard.
#objc private func keyboardWillShow(notification: NSNotification) {
if let userInfo = notification.userInfo {
let keyBoardFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue
let keyboardViewEndFrame = view.convert(keyBoardFrame!, from: view.window)
let keyboardHeight = keyboardViewEndFrame.height
tableView.scrollToBottom() { indexPath in
let rectofCell = self.tableView.rectForRow(at: indexPath)
let lastCellFrame = self.tableView.convert(rectofCell, from: self.view.window)
if lastCellFrame.origin.y + lastCellFrame.size.height > keyboardViewEndFrame.origin.y {
let overlap = lastCellFrame.origin.y + lastCellFrame.size.height - keyboardViewEndFrame.origin.y
self.tableView.frame.origin.y = -overlap
}
}
}
}
extension UITableView {
func scrollToBottom(animated: Bool = true, completion: ((IndexPath) -> Void)? = nil) {
let sections = self.numberOfSections
let rows = self.numberOfRows(inSection: sections - 1)
if (rows > 0){
let indexPath = IndexPath(row: rows - 1, section: sections - 1)
self.scrollToRow(at: indexPath, at: .bottom, animated: true)
completion?(indexPath)
}
}
}
The value for rectOfCell shows (0.0, 5305.333518981934, 375.0, 67.33333587646484) and the converted value (0.0, 9920.000185648601, 375.0, 67.33333587646484) and the table view disappears out of the screen.
I only want to move the table view upward if the last message overlaps with the eventual position of the keyboard. For example, when the keyboard is invoked (either when the table view is already at the bottom or not at the bottom), the table view shouldn't move upward if the appearance of the keyboard doesn't cover the messages (i.e., there is only one message).
You don't need to manage tableView.frame for such a thing. You just need to make sure that when keyboard appears, tableView adds necessary contentInset value from bottom so that user can still see all the content inside tableView with the keyboard still on screen.
All you need is this -
// when keyboard appears
let insets = UIEdgeInsets(top: 0, left: 0, bottom: keyboardHeight, right: 0)
tableView.contentInset = insets
tableView.scrollIndicatorInsets = insets
// when keyboard disappears
let insets: UIEdgeInsets = .zero
tableView.contentInset = insets
tableView.scrollIndicatorInsets = insets
I tried getting the height of the keyboard to update the table contentInsets when the keyboard shows up; but the notification from UIResponder.keyboardWillShowNotification shows the frame with the safe area height. Is there a way to get the actual keyboard frame?
Code for the height of the keyboard height:
if let keyboardFrame: NSValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue {
let keyboardRectangle = keyboardFrame.cgRectValue
let keyboardHeight = keyboardRectangle.height
print("show: \(keyboardHeight)")
tableView.scrollIndicatorInsets = UIEdgeInsets(top: 0, left: 0, bottom: 18 + keyboardHeight, right: 0)
tableView.contentInset = UIEdgeInsets(top: 8, left: 0, bottom: 18 + keyboardHeight, right: 0)
}
But this gives the height of the following keyboard:
https://imgur.com/m0HR088
What I want:
https://imgur.com/Lp6vHOm
First you have to get Safe Area bottom height and then
exclude it from keyboard total height. In this way you will get only
keyboard height without bottom safe area.
pesudo code:
let keyboardHeight = your keyboard height - bottomPadding
if #available(iOS 11.0, *) {
let window = UIApplication.shared.keyWindow
let bottomPadding = window?.safeAreaInsets.bottom
}
Hope it will help you.
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
}
I'm working on a chat that should work on iOS 11 and 12. On iOS 12 everything works as expected. On iOS 11, however, I have the problem that the table view content size increases (no cells) as soon as the keyboard appears. The amount of extra height matches with the keyboard height.
Demo
Here is a demo with iOS 11 on the left and iOS 12 on the right. On iOS 12 everything works just fine. Put attention to the bottom of the table view on iOS 11 when the keyboard appeared.
View/View controller hierarchy setup
- = View controller
+ = View
- UINavigationViewController
- UIViewController // Controlling contentInsets, contentOffset of the tableView
+ UIView
- UITableViewController
+ UITableView
- UIViewController // Controlling the text input bar at the bottom
+ ... // Other views
+ UITextView
Layout Constraints
The table view's anchors are equal to its superview's anchors. So fullscreen, ignoring safe area. So when the keyboard appears the frame doesn't change, but the bottom content insets.
More Details
I've set tableView.contentInsetAdjustmentBehavior = .never
This is how I calculate the insets and offset of the table view when the keyboard appears. It's complex because there are several scenarios where there should be different behavior. There's a similar complex calculation when the keyboard disappears, and when the height of the text input changes. I always want to scroll the table view up or down according to view frame changes.
#objc func handleKeyboardWillShowNotification(_ notification: NSNotification) {
let frameEnd: CGRect = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as AnyObject).cgRectValue ?? .zero
let keyboardHeight = frameEnd.height
let contentHeight = tableView.contentSize.height
let visibleTableViewHeight = tableView.frame.height - (tableView.contentInset.top + tableView.contentInset.bottom)
let distanceToScroll = (keyboardHeight - view.safeAreaInsets.bottom)
var y: CGFloat = 0
if contentHeight > visibleTableViewHeight {
y = tableView.contentOffset.y + distanceToScroll
} else {
let diff = visibleTableViewHeight - contentHeight
let positionAtKeyboard = distanceToScroll - tableView.contentInset.top - diff
y = positionAtKeyboard < tableView.contentInset.top ? -tableView.contentInset.top : positionAtKeyboard
}
let contentOffset = CGPoint(x: 0, y: y)
tableView.contentInset.bottom = keyboardHeight + inputBar.frame.height
tableView.scrollIndicatorInsets = tableView.contentInset
tableView.setContentOffset(contentOffset, animated: false)
}
I also have tried this on different screen sizes and it always adds an amount to the contentSize that matches exactly the height of the keyboard.
You can use following code for keyboard hide and show.
//Show keyboard.
#objc func keyboardWillAppear(_ notification: NSNotification) {
if let newFrame = (notification.userInfo?[ UIResponder.keyboardFrameEndUserInfoKey ] as? NSValue)?.cgRectValue {
if self.tableView.contentInset.bottom == 0 {
let insets: UIEdgeInsets = UIEdgeInsets( top: 0, left: 0, bottom: newFrame.height, right: 0 )
self.tableView.contentInset = insets
self.tableView.scrollIndicatorInsets = insets
UIView.animate(withDuration: 0.1) {
self.view.layoutIfNeeded()
}
}
}
}
//Hide keyboard.
#objc func keyboardWillDisappear(_ notification: NSNotification) {
if self.tableView.contentInset.bottom != 0 {
self.tableView.contentInset = UIEdgeInsets( top: 0, left: 0, bottom: 0, right: 0 )
self.tableView.scrollIndicatorInsets = UIEdgeInsets( top: 0, left: 0, bottom: 0, right: 0 )
UIView.animate(withDuration: 0.1) {
self.view.layoutIfNeeded()
}
}
}
This is work for me.
First of all you don't have to do unnecessary calculation
Simply calculate the keyboard height, and move the keyboard upside.
Swift Version:
#objc func keyboardWillShow(notification: NSNotification) {
if let keyboardSize = (notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue {
self.tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardSize.height + 10, right: 0)
UIView.animate(withDuration: 0.25) {
self.tableView.layoutIfNeeded()
self.view.layoutIfNeeded()
}
}
}
#objc func keyboardWillHide(notification: NSNotification) {
self.tableView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
UIView.animate(withDuration: 0.5) {
self.tableView.layoutIfNeeded()
self.view.layoutIfNeeded()
}
}
Objecttive-C Version:
- (void)keyboardWillShow:(NSNotification *)notification
{
NSDictionary *keyInfo = [notification userInfo];
CGRect keyboardFrame = [[keyInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
self.tableView.contentInset = UIEdgeInsetsMake(0, 0, keyboardFrame.size.height + 10, 0);
[UIView animateWithDuration:0.2 animations:^{
[self.tableView layoutIfNeeded];
[self.view layoutIfNeeded];
} completion:nil];
}
- (void) keyboardWillHide: (NSNotification *) notification
{
self.tableView.contentInset = UIEdgeInsetsMake(0, 0, 0, 0);
[UIView animateWithDuration:0.2 animations:^{
[self.view layoutIfNeeded];
} completion:nil];
}
Let me know If you find any difficulties.
This works for me very well
Workaround
This is not specifically answering the original question, but might be a solution for those who don't have a translucent keyboard and input view.
I could get around this problem by changing the constraints and not setting the bottom insets. Initially the bottom constraint of the table view was set to the bottom of the super view (bottom of the screen, basically). So when the keyboard appeared, I didn't change the table view's frame, but the bottom inset. This apparently didn't work properly.
Now I've set the bottom constraint of the table view to the top of the input view (black bar) and the bottom inset to zero. Since the input view moves up when the keyboard appears, it changes the frame of the table view and the bottom inset remains zero. I still set the content offset, because I need specific behavior in different situations, but that's it.
This only works in my situation, because I neither have a translucent input bar nor keyboard and don't need to show blurry content behind it.
I am having an issue in which relying on convertRect to properly report a y position to use to calculate a contentInset is not working on iOS 12. This approach used to work on earlier iOS versions:
#objc func keyboardVisibilityChanged(notification: Notification) {
guard let userInfo = notification.userInfo else {
assertionFailure()
return
}
let keyboardScreenEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
let keyboardViewEndFrame = scrollView.convert(keyboardScreenEndFrame, from: view.window!)
if notification.name == UIResponder.keyboardWillHideNotification {
scrollView.contentInset = .zero
scrollView.scrollIndicatorInsets = .zero
} else {
let insets = UIEdgeInsets(top: 0, left: 0, bottom: (keyboardViewEndFrame.origin.y - keyboardViewEndFrame.size.height) , right: 0)
scrollView.contentInset = insets
scrollView.scrollIndicatorInsets = insets
}
}
However, this code, while achieving extremely close visual results, is not exact and also breaks on iPhone, where the modal is presented fullscreen.
Apple states in their documentation:
Note: The rectangle contained in the UIKeyboardFrameBeginUserInfoKey
and UIKeyboardFrameEndUserInfoKey properties of the userInfo
dictionary should be used only for the size information it contains.
Do not use the origin of the rectangle (which is always {0.0, 0.0}) in
rectangle-intersection operations. Because the keyboard is animated
into position, the actual bounding rectangle of the keyboard changes
over time.
So I came up with the following solutions that seems to work well on iOS 13, 12 and 11, including safe areas, modal form sheets, and hardware keyboards):
// MARK: - Keyboard Notifications
#objc func keyboardVisibilityChanged(notification: Notification) {
if notification.name == UIResponder.keyboardWillHideNotification {
scrollView.contentInset = .zero
scrollView.scrollIndicatorInsets = .zero
} else {
guard let userInfo = notification.userInfo,
let value = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue,
let window = view.window else {
assertionFailure()
return
}
let keyboardEndFrameInWindowCoordinates = value.cgRectValue
let viewFrameInWindowCoordinates = window.convert(scrollView.frame,
from: scrollView.superview)
let contentInsetBottom: CGFloat
// If the keyboard is below us, no need to do anything.
// This can happen when a hardware keyboard is attached to a modal form sheet on iPad
if keyboardEndFrameInWindowCoordinates.origin.y >= viewFrameInWindowCoordinates.maxY {
contentInsetBottom = 0
} else {
let bottomEdgeOfViewInWindowBottomCoordinates = window.frame.maxY - viewFrameInWindowCoordinates.maxY
contentInsetBottom = keyboardEndFrameInWindowCoordinates.height - bottomEdgeOfViewInWindowBottomCoordinates - view.safeAreaInsets.bottom
}
let insets = UIEdgeInsets(top: 0,
left: 0,
bottom: contentInsetBottom,
right: 0)
scrollView.scrollIndicatorInsets = insets
}
}
So I followed apple guidelines on moving text above the keyboard, this works fine when the textfields are on scrollview, however the scrollview contains a collection view and that collection view loads a nib with textfields populated within it, they are all assigned as the delegate and when they are pressed the didEdit/endEdit functions of the delegate do fire however the keyboard management code doesn't work as expected... here is the keyboard management code
http://creativecoefficient.net/swift/keyboard-management/
heres a link to the code am using..
func keyboardWillBeShown(sender: NSNotification) {
print("KEYBOARD SHOWN")
let info: NSDictionary = sender.userInfo!
let value: NSValue = info.valueForKey(UIKeyboardFrameBeginUserInfoKey) as! NSValue
let keyboardSize: CGSize = value.CGRectValue().size
let contentInsets: UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0,keyboardSize.height, 0.0)
print(keyboardSize.height)
ScrollView.contentInset = contentInsets
ScrollView.scrollIndicatorInsets = contentInsets
// If active text field is hidden by keyboard, scroll it so it's visible
// Your app might not need or want this behavior.
var aRect: CGRect = self.view.frame
aRect.size.height -= keyboardSize.height
let activeTextFieldOrigin: CGPoint? = activeTextFieldRect?.origin
if (!CGRectContainsPoint(aRect, activeTextFieldOrigin!)) {
let dHeight = displayView.frame.height
ScrollView.scrollRectToVisible(activeTextFieldRect!, animated:true)
}
The problem with this code is that the activeTextfield works well with the textfields of the view, i get these points printed when i click on the textfield
activetextfield Frame
(0.0, 20.5, 150.0, 20.5)
but when i click on the collection view nib textfields i get these points
0.0, 0.0, 259.5, 30.0
I believe this is the reason the keyboard is blocking the textfields, the activetextfieldRect is giving the wrong coordinates to
ScrollView.scrollRectToVisible(activeTextFieldRect!, animated:true)
can someone give me some direction on how to go about fixing this?
Yes, you're right. scrollRectToVisible receive wrong coordinates, and this is why it is not work.
To achieve your goal consider using scrollToItemAtIndexPath(_:atScrollPosition:animated:). All what you need is IndexPath of cell to move. And I guess you can put that info in tag property of your textField (for instance).
Something like this:
// inside cellForItem..
cell.textField.tag = indexPath.item
// inside keyboardWillShown
let indexPath = NSIndexPath(forItem: activeTextField.tag inSection: 0) // assume that you have one section
collectionView.scrollToItemAtIndexPath(indexPath, atScrollPosition: .Top, animated: true) // or try .Center position
func keyboardWillBeShown(sender: NSNotification) {
print("KEYBOARD SHOWN")
let info: NSDictionary = sender.userInfo!
let value: NSValue = info.valueForKey(UIKeyboardFrameBeginUserInfoKey) as! NSValue
let keyboardSize: CGSize = value.CGRectValue().size
let contentInsets: UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardSize.height, 0.0)
ScrollView.contentInset = contentInsets
ScrollView.scrollIndicatorInsets = contentInsets
// If active text field is hidden by keyboard, scroll it so it's visible
// Your app might not need or want this behavior.
var aRect: CGRect = self.ScrollView.frame
aRect.size.height -= keyboardSize.height
let activeTextFieldRect: CGRect? = activeTextField?.frame
let activeTextFieldOrigin: CGPoint? = TextfieldPoint
if (!CGRectContainsPoint(aRect, activeTextFieldOrigin!)) {
ScrollView.scrollRectToVisible(testBox, animated:true)
}
}
// Called when the UIKeyboardWillHideNotification is sent
func keyboardWillBeHidden(sender: NSNotification) {
print("KEYBOARD HIDDEN")
let contentInsets: UIEdgeInsets = UIEdgeInsetsZero
ScrollView.contentInset = contentInsets
ScrollView.scrollIndicatorInsets = contentInsets
}
func textFieldDidBeginEditing(textField: UITextField) {
let point: CGPoint = textField.convertPoint(CGPointZero, toView: self.ScrollView);
TextfieldPoint = point
testBox = CGRectMake(point.x, point.y, textField.frame.width, 100 );
//let indexPath:NSIndexPath? = collview1.indexPathForItemAtPoint(point)
print("TEXTFIELD EDIT")
activeTextField = textField
print(activeTextField)
ScrollView.scrollEnabled = true
}
func textFieldDidEndEditing(textField: UITextField) {
print("TEXTFIELD STOP EDIT")
activeTextField = nil
ScrollView.scrollEnabled = false
}
it works just really messy i guess, i got the point where textfield was, made CGRect out of it, if the keyboard overlaps the text, the view adjusts, tested on iPhone 4s to 6s plus, works fine