iOS 7.1 UITextView still not scrolling to cursor/caret after new line

懵懂的女人 提交于 2019-12-17 15:33:51

问题


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).


回答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.




回答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.




回答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!




回答4:


I can't find original source but it works on iOS7.1

 - (void)textViewDidChangeSelection:(UITextView *)textView
 {
   if ([textView.text characterAtIndex:textView.text.length-1] != ' ') {
        textView.text = [textView.text stringByAppendingString:@" "];
    }

    NSRange range0 = textView.selectedRange;
    NSRange range = range0;
    if (range0.location == textView.text.length) {
        range = NSMakeRange(range0.location - 1, range0.length);
    } else if (range0.length > 0 &&
               range0.location + range0.length == textView.text.length) {
        range = NSMakeRange(range0.location, range0.length - 1);
    }
    if (!NSEqualRanges(range, range0)) {
        textView.selectedRange = range;
    }
 }



回答5:


Some one have made a subclass that solves all scrolling0related issues in UITextView. The implementation couldn't be easier - switch UITextView with the subclass PSPDFTextView.

A post about it, showing what is fixed (With nice Gif animations) is here: Fixing UITextView on iOS 7

The git is here: PSPDFTextView



来源:https://stackoverflow.com/questions/22315755/ios-7-1-uitextview-still-not-scrolling-to-cursor-caret-after-new-line

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!