iOS 7.1版本的UITextView在换行后仍无法滚动到光标位置。

25
自从 iOS 7 开始,UITextView 在用户输入跨行文本时不会自动滚动到光标位置。这个问题在 Stack Overflow 和其他地方都有详细记录。对我而言,在 iOS 7.1 中仍存在该问题。我做错了什么?
我安装了 Xcode 5.1 并针对 iOS 7.1 进行设置。我正在使用自动布局。
以下是我如何将文本视图的内容定位在键盘上方:
- (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;
}
我尝试过的方法: 我尝试了许多在SO上发布的与iOS 7相关的解决方案。我所尝试的所有解决方案似乎都不适用于显示属性字符串的文本视图。在以下三个步骤中,我将概述SO上最受欢迎的答案(https://dev59.com/kGMk5IYBdhLWcg3wyw0U#19277383)如何响应用户首次点击返回键。

(1.) 在viewDidLoad中,文本视图成为第一个响应者。滚动到光标所在的文本视图底部。

enter image description here

(2.) 在键盘上没有输入任何字符之前,先点击一下返回键,光标就会消失不见。

enter image description here

(3.) 然而,再次点击返回键似乎可以使情况恢复正常。(注意:删除后面的换行符会再次使光标消失)。

enter image description here


1
需要注意的是在升级到iOS 7.1后,苹果日历应用程序中仍存在滚动问题。创建一个新事件,向下滚动到“备注”部分,重复按回车键直到光标消失。 - bilobatum
你没有做错任何事情。这是一个错误。 - matt
@matt 但是有很多关于这个问题的错误报告被提交给了苹果。也许我们不应该设置由 Text Kit 支持的文本视图的 contentInset。如果我避免设置 contentInset,所谓的 bug 大部分都消失了。 - bilobatum
1
@bilobatum 请查看Peter Steinburger的帖子,他提供了一个很好的解释来解决这个问题 - http://petersteinberger.com/blog/2014/fixing-uitextview-on-ios-7/ - Oliver Atkinson
2
问题已经在iOS 8上得到解决。 - Dmitry
显示剩余4条评论
5个回答

11

改进后的 UITextView 子类代码:

#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;
}

请注意,在使用蓝牙键盘时,向下箭头键无法正常工作。


1
我喜欢它(尽管您的初始化程序不完整)。可能还需要重写setAttributedText:方法。我很高兴iOS 8修复了这个错误。 - bilobatum
初始化程序已修复。 - Dmitry

9
一个强大的解决方案应该在以下情况下适用:
(1.) 显示属性字符串的文本视图
(2.) 通过键盘点击回车键创建的新行
(3.) 输入的文本溢出到下一行而创建的新行
(4.) 复制和粘贴文本
(5.) 第一次通过点击回车键创建的新行(请注意OP中的3个步骤)
(6.) 设备旋转
(7.) 我想不到的其他情况...
为了满足这些要求,在iOS 7.1中,似乎仍然需要手动滚动到插入符号。
通常可以看到解决方案在textViewDidChange:文本视图委托方法被调用时手动滚动到插入符号。然而,我发现这种技术不能满足上述第5种情况。即使在滚动到插入符号之前调用layoutIfNeeded也没有帮助。相反,我必须在CATransaction完成块内滚动到插入符号:
// 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];
    }
}

为什么这个能够工作?我不知道。你需要询问一位苹果工程师。

为了完整起见,这是与我的解决方案相关的所有代码:

#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

如果你遇到这种情况不起作用,请告诉我。

您的解决方案无法使用蓝牙键盘上的向下箭头键。 - Dmitry
如果新文本以“\n”结尾,则它会破坏setText方法 - 文本始终会滚动到末尾。 - Dmitry

3

我通过获取插入符号的实际位置并进行调整来解决了这个问题,以下是我的方法:

- (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];
        }
    }
}

如果只需要在用户按下回车键后定位,可以尝试以下方法:

- (void) textViewDidChange:(UITextView *)textView {
    if ([textView.text hasSuffix:@"\n"]) {
        [self alignTextView:textView withAnimation:NO];
    }
}

如果这个对你有效,请告诉我!


@bilobatum 听起来你的解决方案只是部分解决。文本视图应该始终显示光标,尽管存在操作系统错误。此外,使用 contentInset 比更改视图框架更容易,我个人认为。 - nielsbot
如果用户按下回车键,那么只使用插入符号,查看编辑内容。 - Logan
@Logan 不起作用是因为在调用 textView:shouldChangeTextInRange:replacementText: 时,换行符号还未被添加到文本视图中。 - bilobatum
@Logan 好的,那个方法可行。我会给你一个+1,但我会暂时推迟选择答案。在iOS 7.1中,我真的希望避免手动滚动的hack来完成一些在iOS 6中可以免费完成的事情。 - bilobatum
希望有所收获! - Logan
显示剩余3条评论

0

我找不到原始代码,但它可以在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;
    }
 }

你尝试过使用显示属性字符串的文本视图来解决这个问题吗?我无法让它正常工作。 - bilobatum

0

有人创建了一个子类,解决了UITextView中所有与滚动相关的问题。实现起来非常简单-将UITextView替换为子类PSPDFTextView

关于此的帖子展示了解决了哪些问题(附有漂亮的GIF动画):在iOS 7上修复UITextView

Git地址在这里:PSPDFTextView


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接