wrong order of execution of settings in keyboardWillShow - ios

My ViewController view contains scrollView with bounds of it's superview and there is a tableView at the bottom of the scrollView as it's subview. TableView height constraint is set to it's content size height and above it there is textfield. So when the user touches textField keyboard appears. And in order to the textField could be visible I want to scroll the scrollView up, but usually there is not enough space so I want to increase the tableView height. And the problem is that it behaves like first the scrollView is scrolled and later the tableViewConstraint is set so it never scrolls enough when there is no space. Could I 'dispatch'
tableViewConstraint.constant = keyboardHeight in some way that it will be executed first on UI.
override func viewDidLoad() {
tableViewConstraint.constant = TableView.contentSize.height
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name:NSNotification.Name.UIKeyboardWillShow, object: nil)
...
}
#objc func keyboardWillShow(notification:NSNotification){
if let keyboardFrame: NSValue = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue {
let keyboardRectangle = keyboardFrame.cgRectValue
let keyboardHeight = keyboardRectangle.height
tableViewConstraint.constant = keyboardHeight
let boundsOfOffsetView = scrollView.bounds.offsetBy(dx: CGFloat(0), dy: keyboardHeight)
scrollView.scrollRectToVisible(boundsOfOffsetView, animated: true)
}
}

You can do your layout changes in an animate(withDuration:animations:completion:) block and call layoutIfNeeded() to force the changes to occur at the time of your choosing. I would try something like this to start:
// Within your if statement…
let keyboardRectangle = keyboardFrame.cgRectValue
let keyboardHeight = keyboardRectangle.height
UIView.animate(withDuration: 1.0/3.0) {
tableViewConstraint.constant = keyboardHeight
scrollView.layoutIfNeeded()
}
let boundsOfOffsetView = scrollView.bounds.offsetBy(dx: CGFloat(0), dy: keyboardHeight)
scrollView.scrollRectToVisible(boundsOfOffsetView, animated: true)

Related

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

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

Layout is Broken when I update the Table View Height Constraint programmatically

when I'm trying to update my table view height constraint constant it causes a bug in the view.
What I am doing wrong?
func setupViewHeight() {
// prepare the animator first and keep a reference to it
let animator = UIViewPropertyAnimator(duration: 0.5, timingParameters: UICubicTimingParameters(animationCurve: .linear))
animator.addAnimations {
self.view.layoutIfNeeded()
}
// at some other point in time we change the constraints and call the animator
self.tableHeightConstraint.constant = CGFloat(self.previousClinics.count) * self.cellHeight
self.view.setNeedsLayout()
animator.startAnimation()
self.tableView.reloadData()
}
Wrapper View Subview Constraints
Wrapper View Constraints
Buggy View in Action (video)
You can try one thing -> change tableview height constraint priority to 100 and try to remove all fixed height of cell.
Hope it'll help you.
Try this one.It's work perfectly for me.
let cellHeight = 100.0
var cell : tableCell?
let detailArr : [UIColor] = [.red,.yellow,.black,.blue]
override func viewWillAppear(_ animated: Bool) {
self.changeTableHeight()
}
func changeTableHeight()
{
let animator = UIViewPropertyAnimator(duration: 0.5, timingParameters: UICubicTimingParameters(animationCurve: .linear))
animator.addAnimations {
self.view.layoutIfNeeded()
}
tableviewHeight.constant = CGFloat(Double(detailArr.count) * self.cellHeight)
self.view.setNeedsLayout()
animator.startAnimation()
self.tableview.reloadData()
}

UICollectionView top cells disappear while animating keyboard appearance

I have UICollectionView with cells inside. Everything works well except one thing.
During keyboard appearance animation, top cells that are about to go offscreen just disappear in place without any animation https://www.youtube.com/watch?v=hMt6DiJD5KU
I have tried to implement finalLayoutAttributesForDisappearingItem but it has no any effect.
Also tried to expand rect in layoutAttributesForElementsInRect
override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
let rect = CGRect(x: rect.minX, y: rect.midY - 312, width: rect.width, height: rect.height + 312)
return itemAttributesCache.filter { $0.frame.intersects(rect) }
}
but also with no luck.
The only working solution that i found is to expand UICollectionView.frame above the screen to the keyboard height and set UICollectionView.contentInset.top to the keyboard height. It works but absolutely ugly.
Any ideas how to fix it?
To have a proper keyboard appearance in your collection view:
1) Get the size of keyboard.
2) Adjust the bottom content inset of collection view by keyboard height.
3) Scroll the target text field into view.
You need to register for keyboard changing notifications.
(keyboardWillHideNotification and keyboardWillChangeFrameNotification)
Use notification center:
Inside class:
let notificationCenter = NotificationCenter.default
In viewDidLoad:
notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name:NSNotification.Name.UIKeyboardWillHide, object: nil)
notificationCenter.addObserver(self, selector: #selector(adjustForKeyboard), name: NSNotification.Name.UIKeyboardWillChangeFrame, object: nil)
#objc func adjustForKeyboard(notification: Notification) {
if let keyboardValue = notification.userInfo?[UIKeyboardFrameEndUserInfoKey] as? NSValue {
let keyboardScreenEndFrame = keyboardValue.cgRectValue
let keyboardViewEndFrame = view.convert(keyboardScreenEndFrame, from: view.window)
if notification.name == NSNotification.Name.UIKeyboardWillHide {
collectionView.contentInset = .zero
} else {
collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: keyboardViewEndFrame.height - view.safeAreaInsets.bottom, right: 0)
}
}
}
Reset the content inset when keyboard dismissed.
I'm using textField delegate method.
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
print("returned")
textField.resignFirstResponder()
return true
}
For more details, please refer these links:
https://developer.apple.com/library/archive/documentation/StringsTextFonts/Conceptual/TextAndWebiPhoneOS/KeyboardManagement/KeyboardManagement.html
and
https://www.hackingwithswift.com/example-code/uikit/how-to-adjust-a-uiscrollview-to-fit-the-keyboard

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!

Keyboard management blocking UITextView

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

Resources