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
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
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)
I'm trying to force UITextView to keep caret always on the same fixed height, for example in the 1/4 of screen.
I should behave similar to old typewriters - when user presses enter (or reaches end of line) text should scroll one line up and caret should stay in the same y position and jump to the begining of new line.
I was trying to do it like so, but it behaves unexpectedly, caret jumps randomly sometimes and scrolling is visible, it scrolls itself down and then I scroll it up again with scrollRectToVisible, this do not seem like ideal way of doing it.
How can I achieve such effect? Any library or pod with similar functionality would also be much appreciated.
func setScrollToMiddle() {
if let selectedRange = textView.selectedTextRange {
let caretRect = textView.caretRect(for: selectedRange.start)
let middleOfCaretHeight = caretRect.origin.y + (caretRect.height / 2)
let screenHeight = UIScreen.main.bounds.height
guard let kbSize = self.keyboardSize else { return }
let keyboardHeight = kbSize.height
let visibleTextAreaHeight = screenHeight - keyboardHeight - topMenuView.frame.height
let finalRectY = middleOfCaretHeight - topMenuView.frame.height - (visibleTextAreaHeight / 2)
let finalRect = CGRect(x: 0.0, y: finalRectY, width: textView.frame.width, height: visibleTextAreaHeight)
textView.scrollRectToVisible(finalRect, animated: false)
}
}
Here is what I would do:
First set up these in viewDid load
override func viewDidLoad() {
super.viewDidLoad()
textView.delegate = self
textView.isEditable = true
textView.isScrollEnabled = false
}
Then add this extension:
extension ViewController: UITextViewDelegate {
func trackCaret(_ textView:UITextView){
if let selectedRange = textView.selectedTextRange {
let caretRect = textView.caretRect(for: selectedRange.start)
let yPos = caretRect.origin.y - (textView.frame.height/2)
let xPos = caretRect.origin.x - (textView.frame.width/2)
textView.bounds.origin = CGPoint(x: xPos, y: yPos)
}
}
func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
trackCaret(textView)
return true
}
func textViewDidBeginEditing(_ textView: UITextView) {
trackCaret(textView)
}
func textViewDidEndEditing(_ textView: UITextView) {
// If you don't need to move back to the original position you can leave this out
// textView.bounds.origin = CGPoint.zero
//or if you want smooth animated scroll back then
textView.scrollRectToVisible(CGRect(x:0,
y:0,
width: textView.bounds.width,
height: textView.bounds.height),
animated: true)
}
}
With this you get the typewriter effect without jumping all over the place. The didendEditing method is only there to scroll back to origin 0,0. If you don't need to do it just remove it.
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!
I am trying to implement this image of my implementation.
Where the three buttons below the uilabel are clickable. I have one UIView as a subview of my custom navigation bar view, then two views within that view, one is the uilabel and the second is the uiview of uibuttons i
I've tried implementing solutions from other answers like
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.point(inside: point, with: event) {
self.isUserInteractionEnabled = true
}else{
self.isUserInteractionEnabled = false
}
return super.hitTest(point, with: event)
}
But that didn't work. I noticed that if I tapped above the button near the position of the default uinavigation bar size then the tap is recognized.
The size of my navigation bar is CGSize(width: self.frame.size.width, height: 100) if that helps.
Update
just adding my custom navigation class and it's usage
class CustomNavigationBar: UINavigationBar {
override func sizeThatFits(_ size: CGSize) -> CGSize {
let newSize :CGSize = CGSize(width: self.frame.size.width, height: 100)
return newSize
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if self.point(inside: point, with: event) {
self.isUserInteractionEnabled = true
}else{
self.isUserInteractionEnabled = false
}
return super.hitTest(point, with: event)
}
}
let navigationController = UINavigationController(navigationBarClass: CustomNavigationBar.self, toolbarClass: nil)
navigationController.setViewControllers([mainController], animated: false)
self.window?.rootViewController = navigationController
self.window?.makeKeyAndVisible()
Please check that your navigation bar height was increased or not, that will be the reason stoping complete interaction of Navigation bar, Add following code and check will work fine,
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
let height: CGFloat = 100 //whatever height you want
let bounds = self.navigationController!.navigationBar.bounds
self.navigationController?.navigationBar.frame = CGRect(x: 0, y: 0, width: bounds.width, height: bounds.height + height)
}
Hi you should change the navigationbar frame in viewdidappear only otherwise frame will not effect.
self.navigationController?.navigationBar.frame = CGRect(x: 0, y: 0, width:width, height: height)
Found the problem!
The view, which contained the title and buttons, was set with a frame with a height of 60 instead of the height of my custom navigation bar. Thanks for the help guys.
Lesson here was
After setting custom height for navigation bar make sure it's subView's bounds matches the navigation bar's bounds.