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.