UITextView - Scrolling to selected place - ios

I have a Notes view controller with a large UITextView in it. When the keyboard is active, I've made sure the contentInset is adjusted so that the user can see what's being typed. This works well.
However, if the textView already has a large amount of text in it and the keyboard isn't active yet, when the user taps on text in the lower portion of the textView, the textView doesn't automatically scroll up to show their cursor. As soon as they start typing, the textView scrolls to the appropriate position, but I'd like the textView to scroll to the position of the cursor as soon as they tap in the textView.
Here's my code:
override func viewDidLoad() {
super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWasShown), name: NSNotification.Name.UIKeyboardDidShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillBeHidden), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
}
func keyboardWasShown(_ notification: Notification) {
let mainViewY = self.view.frame.origin.y
let textViewY = self.textView.frame.origin.y
let oneLineHeight = self.textView.font.lineHeight
let delta = (textViewY - mainViewY) - oneLineHeight
let keyboardSize = (notification.userInfo?[UIKeyboardFrameBeginUserInfoKey] as? NSValue)?.cgRectValue
let keyboardHeight = (keyboardSize?.height)!
self.textView.contentInset = UIEdgeInsetsMake(0, 0, keyboardHeight + delta, 0)
self.textView.scrollIndicatorInsets = textView.contentInset
}
func keyboardWillBeHidden(_ notification: Notification) {
self.textView.contentInset = UIEdgeInsets.zero
self.textView.scrollIndicatorInsets = UIEdgeInsets.zero
}
I searched for people asking this question but couldn't find anyone experiencing my particular issue.
How can I ensure that when the user taps the textView to begin typing in a portion of the textView that would be covered by the keyboard, the textView scrolls to show their cursor even before they actually type?

You can achieve this by calling scrollRangeToVisible (docs here) using the text view's selectedRange. That method scrolls the text view to any range of text, and the selectedRange should be at the position of the cursor.

I was having a similar issue, but I tried out your sample code.
In viewDidLoad, I commented out the second observer (self.keyboardWillBeHidden).
When I ran the simulator and selected any part of a large block of text, the textView did automatically scroll to the correct position.

Related

Customize automatic UIScrollView scrolling with UITextField

I would like to customize the scroll-offset when showing the keyboard. As you can see in the GIF, the Textfields are quite close to the keyboard and I would like to have a custom position. The "Name" textfield should have 50px more distance and the "Loan Title" textfield should just scroll to the bottom of my UIScrollView.
To be able to scroll past the keyboard I'm changing the UIScrollView insets. Strangely iOS automatically scrolls to the firstResponder textfield (see GIF).
override func viewDidLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
}
#objc func keyboardWillShow(notification: NSNotification) {
// get the Keyboard size
let userInfo = notification.userInfo!
let keyboardEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
// update edge insets for scrollview
self.mainScrollView.scrollIndicatorInsets.bottom = keyboardEndFrame.height - self.view.layoutMargins.bottom
self.mainScrollView.contentInset.bottom = keyboardEndFrame.height - self.view.layoutMargins.bottom
}
I already tried to use the UITextfieldDelegate method: textFieldDidBeginEditing(_ textField: UITextField)
I also tried to use the Apple way described here: https://stackoverflow.com/a/28813720/7421005
None of these ways let me customize the automatic scroll position. In fact it kind of overrides every attempt. Does anyone know a way to workaround this?
You can prevent your view controller from automatically scrolling by setting automaticallyAdjustsScrollviewInsets to false as described here.
Implementing keyboard avoidance is also pretty straight forward. You can see how to do it here.
I don't believe there is any way to keep the automatic positioning and apply your own custom offset. You could experiment with making text field contained in another larger view and making that larger view the first responder, but that would be a hack at best.
I found a solution by myself. The problem was that the automatic scroll (animation) was interfering with my scrollRectToVisible call. Putting this in async fixed the problem.
It now looks similar to this:
override func viewDidLoad() {
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil)
}
#objc func keyboardWillShow(notification: NSNotification) {
// get the Keyboard size
let userInfo = notification.userInfo!
let keyboardEndFrame = (userInfo[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue
// update edge insets for scrollview
self.mainScrollView.scrollIndicatorInsets.bottom = keyboardEndFrame.height - self.view.layoutMargins.bottom
self.mainScrollView.contentInset.bottom = keyboardEndFrame.height - self.view.layoutMargins.bottom
var frame = CGRect.zero
if nameTextField.isFirstResponder {
frame = CGRect(x: nameTextField.frame.origin.x, y: nameTextField.frame.origin.y + 50, width: nameTextField.frame.size.width, height: nameTextField.frame.size.height)
}
if titleTextField.isFirstResponder {
frame = CGRect(x: titleTextField.frame.origin.x, y: titleTextField.frame.origin.y + titleShortcutsCollectionView.frame.height + 25, width: titleTextField.frame.size.width, height: titleTextField.frame.size.height)
}
DispatchQueue.main.async {
self.mainScrollView.scrollRectToVisible(frame, animated: true)
}
}

Swift 3 : Issue with cursor of textView when touched by the user

I've been struggling with a rather stupid issue for a few days now and I have not been able to find a solution.
I have a view controller with the following hierarchy: xcode screenshot
The user will have the possibility to add pictures below the textView (I haven't implemented this feature yet).
I set the "scrolling enabled" property of my textView to false so that its height automatically increases or decreases depending on the content typed by the user, which is working as expected.
I use the notification center in order to change the scrollView's contentInsets when the keyboard is shown like so:
func keyboardWillShow(notification: NSNotification) {
var userInfo = notification.userInfo!
var keyboardFrame: CGRect = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).cgRectValue
keyboardFrame = self.view.convert(keyboardFrame, from: nil)
var contentInsets: UIEdgeInsets = self.scrollView.contentInset
contentInsets.bottom = keyboardFrame.size.height + 3*self.postContentTextView.font!.lineHeight
self.scrollView.contentInset = contentInsets
}
func keyboardWillHide(notification: NSNotification) {
let contentInsets = UIEdgeInsets.zero
self.scrollView.contentInset = contentInsets
}
with the following observers in viewDidLoad:
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: NSNotification.Name.UIKeyboardWillShow, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: NSNotification.Name.UIKeyboardWillHide, object: nil)
I also set the scroll view content size in the viewDidLayoutSubviews function:
override func viewDidLayoutSubviews() {
self.scrollView.contentSize = self.postContentTextView.frame.size
}
Everything seemed to work fine until I realize that when the text is "too long" (or maybe it is something else), I am not able to select the last lines of the textView when I touch them. Actually, when I touch the screen the cursor is supposed to appear on the line I have just touched. This works normally for three quarters of the textView but it looks like the textView does not detect my finger for the last lines (the bigger the height is, the worse this issue is).
At the beginning, I thought I had a problem with my screen but I tested it on the iOS Simulator and the same problem occurred.
I have to admit that I am completely clueless about it. Does any of you have an idea of what the problem could be ?
Have you checked its constraints? The UITextView might be outside the bounds of its superviews. Set clipsToBounds to superview & test it. If it is out of the superview bounds, it will not detect touch

Showing Textfield when Keyboard is up

I understand that the issue I'm posting has been discussed a lot but as far as I have searched in SO, I couldn't find a solution specific to my issue. I'm posting this to get some inputs.
Problem:
View hierarchy: View -> ScrollView -> View -> Textfield
I have implemented two screens with multiple textfields laid on top of a scrollview to avoid keyboard obscuring the textfield when its at the bottom of the page. I have achieved this using the sample code provided by Apple with minor modifications.
Subscribe to UIKeyboardWillShowNotification and UIKeyboardWillHideNotification
When keyboard is shown, get the height of the keyboard from userInfo dictionary and set appropriate content inset to move the textfield above the Keyboard
When keyboard is hidden, set the content inset to UIEdgeInsetZero to bring back to normal size
The above implementation works perfectly fine when there are more textfields that could occupy the entire screen (assume there are 10 textfields and they extend beyond the frame of the the scroll view). Whenever I tap on a textfield, it moves above the keyboard as expected and I can also scroll the page till the bottom most textfield is visible above the keyboard frame (when the keyboard is still up).
My problem arises when I have only two textfield in the scrollview that are centered vertically in the screen. When I tap on a textfield, the scrollview get an inset equivalent to the keyboard height and it moves above the keyboard as expected but when I scroll the screen, I could see a huge blank space (due to the additional inset) below the textfields.
How can I avoid that blank space when there are only one or two textfields in the page? Should I have to write a different logic to handle that scenario? Any help would be appreciated.
Below is the code:
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillShow:", name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillHide:", name: UIKeyboardWillHideNotification, object: nil)
}
func keyboardWillShow(notification: NSNotification) {
if let userInfo = notification.userInfo {
let keyboardFrame = (userInfo[UIKeyboardFrameEndUserInfoKey] as? NSValue)?.CGRectValue()
if let kbFrame = keyboardFrame {
let inset = CGRectGetHeight(kbFrame)
scrollView.contentInset = UIEdgeInsetsMake(0, 0, inset, 0)
scrollView.scrollIndicatorInsets = UIEdgeInsetsMake(0, 0, inset, 0)
}
}
}
func keyboardWillHide(notfication: NSNotification) {
scrollView.contentInset = UIEdgeInsetsZero
scrollView.scrollIndicatorInsets = UIEdgeInsetsZero
}
TPKeyboardAvoidingScrollView is good, but i recently switched to IQKeyboardManager -> https://github.com/hackiftekhar/IQKeyboardManager
Its upto you to choose but i prefer IQKeyboardManager cause its easy
There are a number of solutions out there, but my preferred solution is to use TPKeyboardAvoiding. To use it here you would just make your UIScrollView an instance or subclass of TPKeyboardAvoidingScrollView in SB or code and it should care of the rest, no other code required!

tableView vs. scrollView behavior when keyboard appears

If a textfield opens keyboard and textfield is member of a tableView, then it will be able to scroll tableView to be able to see the last item, and keyboard will not hide that item.
How? UITableView is inherited from UIScrollView. I guess opening keyboard increases the content offset? Am I right?
If textfield is part of a scrollView, not a tableView, this effect will not occur, and it keyboard can hide out other controls positioned lower parts of the scrollView.
If I want to see the same effect with scrollView as with tableView, should I set content offset manually?
Yes you have to set your content offset manually .
First you register for the notification
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillShow:", name:UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillHide:", name:UIKeyboardWillHideNotification, object: nil)
And then in your observer methods.
func textFieldShouldReturn(textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
func keyboardWillShow(notification:NSNotification){
var userInfo = notification.userInfo!
var keyboardFrame:CGRect = (userInfo[UIKeyboardFrameBeginUserInfoKey] as! NSValue).CGRectValue()
keyboardFrame = self.view.convertRect(keyboardFrame, fromView: nil)
var contentInset:UIEdgeInsets = self.scrollView.contentInset
contentInset.bottom = keyboardFrame.size.height
self.scrollView.contentInset = contentInset
}
func keyboardWillHide(notification:NSNotification){
var contentInset:UIEdgeInsets = UIEdgeInsetsZero
self.scrollView.contentInset = contentInset
}
You can import TPKeyboardAvoiding class downloaded from https://github.com/michaeltyson/TPKeyboardAvoiding and set the UITableView class to TPKeyboardAvoidingTableView.

contentInset of the UIScrollView isn't working

I’m trying to move the content up when the keyboard appears. This is the content which is a simple login form.
Please note that these are not UITextFields. Its just a small UITableView in the middle of a UIViewController. And I have a UIScrollView filling the view controller embedding that table view.
I’ve registered for the keyboard notifications, UIKeyboardDidShowNotification and UIKeyboardWillHideNotification.
And in the method that fires when the keyboard appears, I've implemented a variation of this answer which does pretty much the same thing, setting the bottom value of the UIEdgeInsets which is used to set the contentInset of the scroll view.
func keyboardWasShown(notification: NSNotification) {
var info: NSDictionary = notification.userInfo as NSDictionary
var keyboardSize: CGSize = info.objectForKey(UIKeyboardFrameBeginUserInfoKey).CGRectValue().size
let contentInsets: UIEdgeInsets = UIEdgeInsetsMake(0, 0, keyboardSize.height, 0)
self.scrollView.contentInset = contentInsets
self.scrollView.scrollIndicatorInsets = contentInsets
}
The problem is it doesn't do anything. I successfully get the height of the keyboard but nothing changes. Can anybody tell me why and what I should do to fix this please?
Thank you.
After much struggle, I was able to accomplish what I wanted. What I did was, in the viewDidLoad I registered for the keyboard notifications. And when the keyboard appears, I got its frame in to a variable and the login form's (which is a table view) frame into another variable.
By using CGRectIntersection I got the frame of the intersected portion when the keyboard overlaps the login form and go its height. Then I simply set it in scroll view's setContentOffset method to automatically move the scroll view up. To move it back down to the original position I just set scroll view's contentOffset property again to CGPointZero.
func registerForKeyboardNotifications() {
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWasShown:", name: UIKeyboardDidShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self, selector: "keyboardWillBeHidden:", name: UIKeyboardWillHideNotification, object: nil)
}
func keyboardWasShown(notification: NSNotification) {
var info: NSDictionary = notification.userInfo as NSDictionary
var keyboardFrame: CGRect = info.objectForKey(UIKeyboardFrameEndUserInfoKey).CGRectValue()
var loginFormFrame: CGRect = self.view.convertRect(self.tableView.frame, fromView: nil)
var coveredFrame: CGRect = CGRectIntersection(loginFormFrame, keyboardFrame)
self.scrollView.setContentOffset(CGPointMake(0, coveredFrame.height + 20), animated: true)
}
func keyboardWillBeHidden(notification: NSNotification) {
self.scrollView.contentOffset = CGPointZero
}
UIEdgeInsets UIEdgeInsetsMake (
CGFloat top,
CGFloat left,
CGFloat bottom,
CGFloat right
);
You are setting the bottom. Try setting the top insets. Maybe a negative value will help.
ContentInset means The distance that the content view is inset from the enclosing scroll view. What you need to do is to change the origin.y value.
See How to make a UITextField move up when keyboard is present?

Resources