How to prevent the last table cell from overlapping with the keyboard - ios

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

Related

UITableView adds height of keyboard to contentSize when the keyboard appears on iOS11

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.

How to calculate proper keyboard contentInset for UIScrollView inside of a modally presented form sheet UIViewController

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
}
}

Want ability to manually scroll to pull textfield above keyboard

The problem is apparently when a user sees that the keyboard is blocking textfields that they want to type in, instead of hitting return, or closing the keyboard and tapping on the next field, they try to pan to the next field. Since my content matches the size of the iPad, the scrollview doesn't automatically scroll when the user tries to pan. Honestly, I don't want it to scroll unless the keyboard is on-screen anyway.
However, enabling scrolling on the scrollview doesn't solve the problem; it still won't respond to panning even in that case. Neither does making the viewcontroller the delegate of the scrollview and overriding the function scrollViewDidScroll. How do I get the scrollview to enable panning, particularly only when the keyboard is enabled?
Since a solution has been posted that doesn't quite work, I think I will post my keyboardWillBeShown and keyboardWillBeHidden code:
func keyboardWillBeShown(_ sender: Notification)
{
self.myScrollView.isScrollEnabled = true
let info: NSDictionary = (sender as NSNotification).userInfo! as NSDictionary
let value: NSValue = info.value(forKey: UIKeyboardFrameBeginUserInfoKey) as! NSValue
let keyboardSize: CGSize = value.cgRectValue.size
let contentInsets: UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardSize.height, 0.0)
self.myScrollView.contentInset = contentInsets
self.myScrollView.scrollIndicatorInsets = contentInsets
self.myScrollView.scrollRectToVisible(self.myScrollView.frame, animated: true)
var aRect: CGRect = self.view.frame
aRect.size.height -= keyboardSize.height
let activeTextFieldRect: CGRect? = activeField?.frame
let activeTextFieldOrigin: CGPoint? = activeTextFieldRect?.origin
if activeTextFieldOrigin != nil
{
if (!aRect.contains(activeTextFieldOrigin!))
{
let scrollpoint : CGPoint = CGPoint(x: 0.0, y: activeField!.frame.origin.y - keyboardSize.height)
self.myScrollView.setContentOffset(scrollpoint, animated: true)//.scrollRectToVisible((activeField?.frame)!, animated: true)
}
}
}
func keyboardWillBeHidden(_ sender: Notification)
{
myScrollView.isScrollEnabled = false
let contentInsets: UIEdgeInsets = UIEdgeInsets.zero
myScrollView.contentInset = contentInsets
myScrollView.scrollIndicatorInsets = contentInsets
}
Try this,
Declare variables to be used
var originalViewCGRect: CGRect?
var originalOffset: CGPoint!
in viewDidLoad add keyboard observers
NotificationCenter.default.addObserver(self
, selector: #selector(keyboardWillAppear(_:))
, name: NSNotification.Name.UIKeyboardWillShow
, object: nil)
NotificationCenter.default.addObserver(self
, selector: #selector(keyboardWillDisappear(_:))
, name: NSNotification.Name.UIKeyboardWillHide
, object: nil)
And finally, add these functions
func keyboardWillAppear(_ notification: Foundation.Notification){
self.scrollView.scrollRectToVisible(self.scrollView.frame, animated: true)
let keyboardSize:CGSize = ((notification as NSNotification).userInfo![UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue.size
let insets = UIEdgeInsetsMake(0, 0, keyboardSize.height, 0)
self.scrollView.contentInset = insets
}
func keyboardWillDisappear(_ notification: Foundation.Notification){
let beginFrame = ((notification as NSNotification).userInfo![UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
let endFrame = ((notification as NSNotification).userInfo![UIKeyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
let delta = (endFrame.origin.y - beginFrame.origin.y)
self.view.frame = self.originalViewCGRect!
self.scrollView.setContentOffset(CGPoint(x: 0, y: max(self.scrollView.contentOffset.y - delta, 0)), animated: true)
}
In summary, this will adjust the content inset of the scrollview when the keyboard is shown and/or hidden.
Hope this helps.
EDIT
Fixed adjustments
I figured out how to get what I want. Set myScrollView.contentSize to be as big as the screen plus the keyboard when the keyboard is shown, and reduce the size back to what it was when keyboard is hidden. Then make the view controller a UIScrollViewDelegate, set myScrollView.delegate = self, and implement scrollViewDidScroll so that if a textfield is editing, and that textfield isn't blank, then change which text field is the first responder. Piece of cake once you realize the trick of setting .contentSize!

Strange scrolling of UITableView while scrolling and cell height change are executed

I have a UITableView with a lot of cells. On the cells that have a UITextField i scroll the table if the keyboard would hide the textfield. And to some cells i added a change of cell height to the scroll.
This works fine at the beginning and middle of the table but if the cell in question is at the bottom of the table I am getting some strange jumping around and vanishing of cells.
After the user has finished the editing the height of the cell is changed back to normal and the vanished cells reappear but the table is scrolled a bit upwards.
My question is now how can I get rid of this jumping and vanishing of cells?
Here is my code:
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
if fieldIndex != [-99, -99] {
if indexPath.section == fieldIndex[0] && indexPath.row == fieldIndex[1] {
fieldIndex = [-99, -99]
return 220.0
}
}
return 80.0
}
func keyboardWillBeShown(_ sender: Notification) {
let info: NSDictionary = (sender as NSNotification).userInfo! as NSDictionary
let value: NSValue = info.value(forKey: UIKeyboardFrameBeginUserInfoKey) as! NSValue
let keyboardSize: CGSize = value.cgRectValue.size
let height = toolBar.frame.height
let contentInset: UIEdgeInsets = UIEdgeInsetsMake(0.0, 0.0, keyboardSize.height + toolBar.frame.height + 60, 0.0)
originalIntersect = tableView.contentInset
tableView.contentInset = contentInset
tableView.scrollIndicatorInsets = contentInset
var aRect: CGRect = self.view.frame
aRect.size.height -= keyboardSize.height
}
func keyboardWillBeHidden(_ sender: Notification) {
let insets: UIEdgeInsets = UIEdgeInsetsMake(self.tableView.contentInset.top, 0, 0, 0)
tableView.contentInset = originalIntersect
tableView.scrollIndicatorInsets = originalIntersect
}
If there is other code you need to see to help just ask.
Regards and Thanks
Adarkas

Swift 2.0 UITableView Section Header scroll to top when tapped

So I have checked everywhere and can't seem to find the answer I'm looking for. Here is my issue: I have a UITableView with different sections in it. Each section has a header that when tapped on, it expands that section and reveals it's rows or cells. However, when you tap on the header, it expands it's section down, but it stays in it's spot. I want that section header to move to the top when clicked. Below is the example code. I hope I explained this well.
Here is the section header itself:
func configureHeader(view: UIView, section: Int) {
view.backgroundColor = UIColor.whiteColor()
view.tag = section
let headerString = UILabel(frame: CGRect(x: 40, y: 15, width: tableView.frame.size.width-10, height: 40)) as UILabel
headerString.text = sectionTitleArray.objectAtIndex(section) as? String
headerString.textAlignment = .Left
headerString.font = UIFont.systemFontOfSize(24.0)
view .addSubview(headerString)
let frame = CGRectMake(5, view.frame.size.height/2, 20, 20)
let headerPicView = UIImageView(frame: frame)
headerPicView.image = headerPic
view.addSubview(headerPicView)
let headerTapped = UITapGestureRecognizer (target: self, action:"sectionHeaderTapped:")
view .addGestureRecognizer(headerTapped)
}
Here is the sectionHeaderTapped function:
func sectionHeaderTapped(recognizer: UITapGestureRecognizer) {
print("Tapping working")
print(recognizer.view?.tag)
let indexPath : NSIndexPath = NSIndexPath(forRow: 0, inSection:(recognizer.view?.tag as Int!)!)
if (indexPath.row == 0) {
var collapsed = arrayForBool .objectAtIndex(indexPath.section).boolValue
collapsed = !collapsed;
arrayForBool .replaceObjectAtIndex(indexPath.section, withObject: collapsed)
//reload specific section animated
let range = NSMakeRange(indexPath.section, 1)
let sectionToReload = NSIndexSet(indexesInRange: range)
self.tableView .reloadSections(sectionToReload, withRowAnimation:UITableViewRowAnimation.Fade)
}
}
Thanks!!
You can use scrollToRowAtIndexPath(_:atScrollPosition:animated:) on the table view to scroll the first row of said section to the top position.
One good way is to use IndexPath with a value of row "NSNotFoud", to go to the top of the section 😁
let indexPath = IndexPath(row: NSNotFound, section: 0)
self.scrollToRow(at: indexPath, at: .top, animated: true)

Resources