iOS 7.1 UITextView still not scrolling to cursor/caret after new line
Solution 1
Improved solution's code for UITextView
descendant class:
#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
#define is_iOS7 SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"7.0")
#define is_iOS8 SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0")
@implementation MyTextView {
BOOL settingText;
}
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleTextViewDidChangeNotification:) name:UITextViewTextDidChangeNotification object:self];
}
return self;
}
- (void)scrollToCaretInTextView:(UITextView *)textView animated:(BOOL)animated {
CGRect rect = [textView caretRectForPosition:textView.selectedTextRange.end];
rect.size.height += textView.textContainerInset.bottom;
[textView scrollRectToVisible:rect animated:animated];
}
- (void)handleTextViewDidChangeNotification:(NSNotification *)notification {
if (notification.object == self && is_iOS7 && !is_iOS8 && !settingText) {
UITextView *textView = self;
if ([textView.text hasSuffix:@"\n"]) {
[CATransaction setCompletionBlock:^{
[self scrollToCaretInTextView:textView animated:NO];
}];
} else {
[self scrollToCaretInTextView:textView animated:NO];
}
}
}
- (void)setText:(NSString *)text {
settingText = YES;
[super setText:text];
settingText = NO;
}
Note it doesn't work when Down key is pressed on Bluetooth keyboard.
Solution 2
A robust solution should hold up in the following situations:
(1.) a text view displaying an attributed string
(2.) a new line created by tapping the return key on the keyboard
(3.) a new line created by typing text that overflows to the next line
(4.) copy and paste text
(5.) a new line created by tapping the return key for the first time (see the 3 steps in the OP)
(6.) device rotation
(7.) some case I can't think of that you will...
To satisfy these requirements in iOS 7.1, it seems as though it's still necessary to manually scroll to the caret.
It's common to see solutions that manually scroll to the caret when the text view delegate method textViewDidChange: is called. However, I found that this technique did not satisfy situation #5 above. Even a call to layoutIfNeeded
before scrolling to the caret didn't help. Instead, I had to scroll to the caret inside a CATransaction
completion block:
// this seems to satisfy all of the requirements listed above–if you are targeting iOS 7.1
- (void)textViewDidChange:(UITextView *)textView
{
if ([textView.text hasSuffix:@"\n"]) {
[CATransaction setCompletionBlock:^{
[self scrollToCaretInTextView:textView animated:NO];
}];
} else {
[self scrollToCaretInTextView:textView animated:NO];
}
}
Why does this work? I have no idea. You'll have to ask an Apple engineer.
For completeness, here's all of the code related to my solution:
#import "ViewController.h"
@interface ViewController () <UITextViewDelegate>
@property (weak, nonatomic) IBOutlet UITextView *textView; // full-screen
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
NSString *string = @"All work and no play makes Jack a dull boy.\n\nAll work and no play makes Jack a dull boy. All work and no play makes Jack a dull boy. All work and no play makes Jack a dull boy. All work and no play makes Jack a dull boy. All work and no play makes Jack a dull boy. All work and no play makes Jack a dull boy. All work and no play makes Jack a dull boy. All work and no play makes Jack a dull boy.";
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string attributes:@{NSFontAttributeName: [UIFont fontWithName:@"Verdana" size:30.0]}];
self.textView.attributedText = attrString;
self.textView.delegate = self;
self.textView.backgroundColor = [UIColor yellowColor];
[self.textView becomeFirstResponder];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardIsUp:) name:UIKeyboardDidShowNotification object:nil];
}
// helper method
- (void)scrollToCaretInTextView:(UITextView *)textView animated:(BOOL)animated
{
CGRect rect = [textView caretRectForPosition:textView.selectedTextRange.end];
rect.size.height += textView.textContainerInset.bottom;
[textView scrollRectToVisible:rect animated:animated];
}
- (void)keyboardIsUp:(NSNotification *)notification
{
NSDictionary *info = [notification userInfo];
CGRect keyboardRect = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
keyboardRect = [self.view convertRect:keyboardRect fromView:nil];
UIEdgeInsets inset = self.textView.contentInset;
inset.bottom = keyboardRect.size.height;
self.textView.contentInset = inset;
self.textView.scrollIndicatorInsets = inset;
[self scrollToCaretInTextView:self.textView animated:YES];
}
- (void)textViewDidChange:(UITextView *)textView
{
if ([textView.text hasSuffix:@"\n"]) {
[CATransaction setCompletionBlock:^{
[self scrollToCaretInTextView:textView animated:NO];
}];
} else {
[self scrollToCaretInTextView:textView animated:NO];
}
}
@end
If you find a situation where this doesn't work, please let me know.
Solution 3
I solved it by getting the actual position of the caret and adjusting to it, here's my method:
- (void) alignTextView:(UITextView *)textView withAnimation:(BOOL)shouldAnimate {
// where the blinky caret is
CGRect caretRect = [textView caretRectForPosition:textView.selectedTextRange.start];
CGFloat offscreen = caretRect.origin.y + caretRect.size.height - (textView.contentOffset.y + textView.bounds.size.height - textView.contentInset.bottom - textView.contentInset.top);
CGPoint offsetP = textView.contentOffset;
offsetP.y += offscreen + 3; // 3 px -- margin puts caret 3 px above bottom
if (offsetP.y >= 0) {
if (shouldAnimate) {
[UIView animateWithDuration:0.2 animations:^{
[textView setContentOffset:offsetP];
}];
}
else {
[textView setContentOffset:offsetP];
}
}
}
If you only need to orient after the user presses return / enter, try:
- (void) textViewDidChange:(UITextView *)textView {
if ([textView.text hasSuffix:@"\n"]) {
[self alignTextView:textView withAnimation:NO];
}
}
Let me know if it works for you!
Related videos on Youtube
Comments
-
bilobatum almost 2 years
Since iOS 7, a
UITextView
does not scroll automatically to the cursor as the user types text that flows to a new line. This issue is well documented on SO and elsewhere. For me, the issue is still present in iOS 7.1. What am I doing wrong?I installed Xcode 5.1 and targeted iOS 7.1. I'm using Auto Layout.
Here's how I position the text view's content above the keyboard:
- (void)keyboardUp:(NSNotification *)notification { NSDictionary *info = [notification userInfo]; CGRect keyboardRect = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue]; keyboardRect = [self.view convertRect:keyboardRect fromView:nil]; UIEdgeInsets contentInset = self.textView.contentInset; contentInset.bottom = keyboardRect.size.height; self.textView.contentInset = contentInset; }
What I have tried: I have tried many of the solutions posted to SO on this issue as it pertains to iOS 7. All of the solutions that I have tried do not seem to hold up well for text views displaying an attributed string. In the following three steps, I outline how the most up-voted answer on SO (https://stackoverflow.com/a/19277383/1239263) responds to the user tapping the return key for the first time.
(1.) The text view became the first responder in
viewDidLoad
. Scroll to the bottom of the text view where the cursor is located.(2.) Before typing a single character, tap the return key on the keyboard. The caret disappears out of sight.
(3.) Tapping the return key again, however, seems to normalize the situation. (Note: deleting the latter new line, however, makes the caret disappear once again).
-
Oliver Atkinson about 10 years@bilobatum Check out peter steinburgers post, this is exactly the problem he solves with a great explanation - petersteinberger.com/blog/2014/fixing-uitextview-on-ios-7
-
Dmitry almost 10 yearsThe issue is FIXED on iOS 8.
-
bilobatumIt should also be noted that this scroll issue is still present in Apple's Calendar app after upgrading to iOS 7.1. Create a new event, scroll down to the "Notes" section, hit the return key repeatedly until the cursor disappears.
-
-
nielsbot about 10 years@bilobatum sounds like your solution is only a partial solution. the text view should always show the cursor despite the OS bug. In addition, using contentInset is easier that changing the view frame IMO.
-
Logan about 10 yearsWhat if you only use the caret if user presses enter, see edits.
-
bilobatum about 10 years@Logan Doesn't work because the new line character is not yet set down in the text view when textView:shouldChangeTextInRange:replacementText: is called.
-
bilobatum about 10 years@Logan Ok, that works. I'll give you a +1, but I'm going to defer selecting an answer for a bit. With iOS 7.1, I was really hoping to avoid the manual scrolling hacks to do something that was available for free in iOS 6.
-
Logan about 10 yearsHopefully something arises!
-
bilobatum about 10 yearsHave you tried this solution with a text view that displays an attributed string? I couldn't get it to work.
-
bilobatum about 10 years@Logan I found a problem with your solution when the text view displays an attributed string. The first tap on the return key hides the caret behind the keyboard.
-
Logan about 10 yearsThat's very strange, I wonder what about the attributed string would cause that. Is it going completely off screen?
-
bilobatum about 10 years@Logan Yes, it disappears completely.
-
Dmitry almost 10 yearsYour solution doesn't work with Down key on Bluetooth keyboard.
-
Dmitry almost 10 yearsIt breaks setText method - text always will be scrolled to the end if the new text has @"\n" suffix.
-
bilobatum almost 10 yearsI like it (although your initializer is incomplete). Presumably setAttributedText: will also have to be overridden. I'm glad iOS 8 fixes this bug.
-
Dmitry almost 10 yearsInitializer was fixed.