iOS7中UITextView的contentSize变化和NSLayoutManager

9
问题:在某些情况下,UITextView 会默默地更改其 contentSize
最简单的情况是当有大段文本和键盘时。只需添加 UITextView outlet,并将 - viewDidLoad 设置为以下内容:
- (void)viewDidLoad {
    [super viewDidLoad];
    // expand default "Lorem..."
    _textView.text = [NSString stringWithFormat:@"1%@\n\n2%@\n\n3%@\n\n4%@\n\n5", _textView.text, _textView.text, _textView.text, _textView.text];
    _textView.keyboardDismissMode = UIScrollViewKeyboardDismissModeInteractive;
    _textView.contentInset = UIEdgeInsetsMake(0, 0, 216, 0);
}

现在,在某些情况下,显示或隐藏键盘会导致文本跳动。

我通过子类化 UITextView 找到了跳动的原因。我的子类中唯一的方法是:

- (void)setContentSize:(CGSize)contentSize {
    NSLog(@"CS: %@", NSStringFromCGSize(contentSize));
    [super setContentSize:contentSize];
}

当键盘隐藏时,contentSize 会缩小或扩展。类似于这样:

013-09-16 14:40:27.305 textView-bug2[11087:a0b] CS: {320, 651}
2013-09-16 14:40:27.313 textView-bug2[11087:a0b] CS: {320, 885}
2013-09-16 14:40:27.318 textView-bug2[11087:a0b] CS: {320, 902}

看起来iOS7中UITextView的行为发生了很大变化。现在有些东西坏掉了。

进一步探索后,我发现我的textView的新layoutManager属性也改变了。现在日志中有一些有趣的信息:

2013-09-16 14:41:59.352 textView-bug2[11115:a0b] CS: {320, 668}
<NSLayoutManager: 0x899e800>
    1 containers, text backing has 2129 characters
    Currently holding 2129 glyphs.
    Glyph tree contents:  2129 characters, 2129 glyphs, 3 nodes, 96 node bytes, 5440 storage bytes, 5536 total bytes, 2.60 bytes per character, 2.60 bytes per glyph
    Layout tree contents:  2129 characters, 2129 glyphs, 532 laid glyphs, 13 laid line fragments, 4 nodes, 128 node bytes, 1048 storage bytes, 1176 total bytes, 0.55 bytes per character, 0.55 bytes per glyph, 40.92 laid glyphs per laid line fragment, 90.46 bytes per laid line fragment

下一行内容的contentSize为{320, 885}, 包含Layout tree contents: ..., 2127 laid glyphs, 51 laid line fragments。看起来像是某种自动布局在键盘显示时尝试重新布局textView并更改了contentSize,即使布局尚未完成。即使在键盘显示/隐藏之间我的textView没有变化,它仍然运行。

问题是:如何防止contentSize发生变化?

3个回答

7

看起来问题出在UITextView的默认布局管理器中。我决定对其进行子类化,查看重新布局是何时和为什么被启动的。但是,使用默认设置创建NSLayoutManager解决了问题。

这是我的演示项目中(见问题)的代码(不完美)。那里的_textView是一个输出口,所以我将其从父视图中删除。此代码放置在-viewDidLoad中:

NSTextStorage* textStorage = [[NSTextStorage alloc] initWithString:_textView.text];
NSLayoutManager* layoutManager = [NSLayoutManager new];
[textStorage addLayoutManager:layoutManager];
_textContainer = [[NSTextContainer alloc] initWithSize:self.view.bounds.size];
[layoutManager addTextContainer:_textContainer];
[_textView removeFromSuperview];    // remove original textView
_textView = [[MyTextView alloc] initWithFrame:self.view.bounds 
                                textContainer:_textContainer];
[self.view addSubview:_textView];

MyTextView 在此是 UITextView 的一个子类,请参考问题了解详情。

更多信息请参见:


很不幸,这对我不起作用。我按照这里所说的做法,只是使用自定义文本存储,但是当视图滚动时,contentSize仍然会发生变化。 - Joshua
看起来这个方法能够工作的原因是你已经将文本容器的高度限制为视图的高度。但是当调用其他方法(如lineFragmentUsedRectForGlyphAtIndex)时,这对于布局管理器并不友好,因为它会忽略任何超出文本容器大小的内容。 - Joshua
2
经过一些研究,发现如果文本容器的高度超过9999999,则会出现跳跃现象,因此任何高度超过1000万(10 000 000)的容器都会导致跳跃。我不确定为什么这个数字很特殊,但事实就是如此。 - Joshua
这对我很有效。不知道为什么它起作用了,但我在遵循相同的逻辑时最终到达了这里,但是仅仅将默认文本容器替换为此处创建的容器就解决了问题。 - user1043479
@zxcat 这有点离题,但仍然:NSLayoutManager提供了在其中拥有多个NSTextContainer的能力。我们如何将具有多个文本容器的布局管理器“应用”到UITextView中,而UITextView只能添加一个NSTextContainer? - Dima Deplov
@flinth,我回答不了你的问题。我只是将NSLayoutManager作为iOS7 bug的解决方法使用过,并没有深入研究它。(实际上这个bug在iOS8或9中已经修复了) - zxcat

1
我遇到了与你类似的情况。我的问题显示为不同的错误,但是原因相同:contentSize属性被iOS7错误地静默更改。这是我如何解决它的方法。这是一个有点丑陋的修复方法。每当我需要使用textView.contentSize时,我自己计算它。
-(CGSize)sizeOfText:(NSString *)textToMesure widthOfTextView:(CGFloat)width withFont:(UIFont*)font
{
    CGSize size = [textToMesure sizeWithFont:font constrainedToSize:CGSizeMake(width-20.0, FLT_MAX) lineBreakMode:NSLineBreakByWordWrapping];
    return size;
}

然后您只需调用此函数即可获取大小:
CGSize cont_size =   [self sizeOfText:self.text widthOfTextView:self.frame.size.width withFont:[UIFont systemFontOfSize:15]];

那么,请不要执行以下操作:
self.contentSize = cont_size;// it causes iOS halt occasionally.

所以,直接使用cont_size即可。 我认为目前这是iOS7的一个bug。希望苹果能尽快修复它。 希望这对你有帮助。


谢谢你的回答。但是我自己不使用contentSize。UITextView使用它(内容大小的更改会导致UITextView中的“文本跳跃”)。因此,我需要防止更改而不是在读取时获取正确的contentSize值。 - zxcat
我尝试过的另一个解决方法是重写 setContentSize:,并在布局进行时不更改值。但这真的很糟糕。所以现在我使用了一种解决方法,即创建并附加 NSLayoutManagerUITextView,而不是使用内置的布局管理器。坏处是:这样的 UITextView 无法放置在 storyboard/xib 中。 - zxcat
@zxcat,我尝试了你的layoutManger解决方法。它对我也有效。但有一件事我不明白。_textView = [[MyTextView alloc] initWithFrame:.....在MyTextView类中没有调用initWithFrame。我必须在viewDidLoad中进行初始工作。为什么? - long long
1
  1. MyTextView不是必需的,它只是问题的一部分。您可以使用普通的UITextView
  2. 如果您有自己的UITextView子类,请确保覆盖-initWithFrame:textContainer:,而不是简单的initWithFrame:
- zxcat
啊,你说得对。我这近视的眼睛没看到第二个参数“textContainer”! - long long

1
似乎是iOS7中的错误。在输入文本内容区域的行为在iOS7中非常奇怪,但在较低版本的iOS7上运行良好。
我已添加下面的UITextView委托方法来解决这个问题:
- (void)textViewDidChange:(UITextView *)textView {
CGRect line = [textView caretRectForPosition:
    textView.selectedTextRange.start];
CGFloat overflow = line.origin.y + line.size.height
    - ( textView.contentOffset.y + textView.bounds.size.height
    - textView.contentInset.bottom - textView.contentInset.top );
if ( overflow > 0 ) {
// We are at the bottom of the visible text and introduced a line feed, scroll down (iOS 7 does not do it)
// Scroll caret to visible area
    CGPoint offset = textView.contentOffset;
    offset.y += overflow + 7; // leave 7 pixels margin
// Cannot animate with setContentOffset:animated: or caret will not appear
    [UIView animateWithDuration:.2 animations:^{
        [textView setContentOffset:offset];
    }];
}

感谢修复!现在我的文本视图在编辑时并且处于最后显示的行时可以正常滚动了! - nicolas

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