9

The problem: UITextView silently changes it's contentSize in some situations.

The simplest case textView with large text and keyboard. Just add UITextView outlet and set - viewDidLoad as:

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

Now showing and hiding keyboard will cause text jumps in some cases.

I've found the reason of jumping by subclass UITextView. The only method in my subclass is:

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

And it show contentSize shrinks and expands on keyboard hide. Something like this:

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}

Looks like behavior of UITextView was changed a lot in iOS7. And some things are broken now.

Discovering further I've found that new layoutManager property of my textView changes too. There some interesting info in log now:

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

And next line with contentSize = {320, 885} contains Layout tree contents: ..., 2127 laid glyphs, 51 laid line fragments. So it looks like some kind of autolayout tries to re-layout textView on keyboard show and changes contentSize even if layout is not finished yet. And it runs even if my textView is not changes between keyboard show/hide.

The question is: how to prevent contentSize changes?

zxcat
  • 2,054
  • 3
  • 26
  • 40

3 Answers3

7

Looks like the problem is in default layoutManager of UITextView. I've decided to subclass it and see, where and why re-layout being initiated. But simple creation of NSLayoutManager with default settings solved the problem.

Here is code (not perfect) from my demo project (see in question). The _textView there was an outlet, so I remove it from superview. This code is placed in - 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 here is a subclass of UITextView, see question for details.

For more info see:

zxcat
  • 2,054
  • 3
  • 26
  • 40
  • Unfortunately this doesn't work for me. I'm doing as said here just with custom text storage and the contentSize still changes as the view is scrolled. – Joshua Oct 14 '13 at 19:08
  • It seems the reason this works is because you have set the text container height to be restricted to the height of the view. This does not play well for the layout manager when calling other methods such as `lineFragmentUsedRectForGlyphAtIndex` as it simply ignores any outside of the size of the text container. – Joshua Oct 18 '13 at 10:29
  • 2
    After some research the jumping seems to occur if the height of the text container is above `9999999`, so anything above and including 10 million (`10 000 000`) caused the jumping to occur. I am unsure why this number is special but this seems to be how it is. – Joshua Oct 18 '13 at 12:47
  • This worked great for me. No idea why it works but I ended up here whilst following the same logic, but simply replacing the default text container with the one created here solved the problem. – user1043479 Jan 29 '14 at 15:41
  • @zxcat a bit offtopic here, but still: NSLayoutManager gives the ability to have more than one NSTextContainer in it. How we can "apply" the layout manager with multiple text containers to a UITextView, which can add only one NSTextContainer? – Dima Deplov Jul 10 '16 at 00:36
  • @flinth, I can't answer your question. I've used NSLayoutManager only as workaround of iOS7 bug and never researched it deeply. (Actually that bug is fixed in iOS8 or 9) – zxcat Aug 18 '16 at 12:35
1

I met a similar situation as urs. Mine shows with a different bug but due to the same reason: the contentSize property is silently changed by iOS7 incorrectly. Here is how I work around it. Its kinda a ugly fix. Whenever I need to use textView.contentSize, I calculate it by myself.

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

then you can just call this function to get the size:

CGSize cont_size =   [self sizeOfText:self.text widthOfTextView:self.frame.size.width withFont:[UIFont systemFontOfSize:15]];

then, don't do the following:

self.contentSize = cont_size;// it causes iOS halt occasionally.

so, just use cont_size directly. I believe it's bug in iOS7 for now. Hopefully apple will fix it soon. Hope this is helpful.

long long
  • 122
  • 8
  • Thanks for your answer. But I don't use contentSize myself. `UITextView` uses it (content size changes cause "text jumping" in `UITextView`). So I need to prevent changes instead of obtaining the right contentSize value on read. – zxcat Oct 01 '13 at 13:20
  • Another workaround I've tried was overriding `setContentSize:` and don't change the value if layouting is in progress. But it's really dirty hack. So now I use workaround with creating and attaching `NSLayoutManager` to `UITextView` instead of using build-in layout manager. The bad side: such `UITextView` can't be placed in storyboard/xib – zxcat Oct 01 '13 at 13:24
  • @zxcat, I tried your layoutManger workaround. It works for me too. But there's a thing I don't get it. _textView = [[MyTextView alloc] initWithFrame:..... It doesn't call initWithFrame in MyTextView class. I have to do the initial work in viewDidLoad. Why is that? – long long Oct 01 '13 at 14:18
  • 1
    1. MyTextView is not neccessary, it was a part of question. You may use normal `UITextView`. 2. If you have your own `UITextView` subclass, make sure to override `-initWithFrame:textContainer:`, not simple `initWithFrame:` – zxcat Oct 01 '13 at 16:14
  • Ah you are right. My near-sighted eyes didn't see the second param "textContainer"! – long long Oct 01 '13 at 17:31
1

Seems the bug in iOS7. At the time of input text content area behavior is wired in iOS7, It works fine with lower iOS7 version.

I have added below delegate method of UITextView to resolved this issue :

- (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];
    }];
}
torap
  • 656
  • 6
  • 15
  • Thanks for the fix! My text view is now scrolling well again when editing and being at the last displayed line! – nicolas Jan 15 '14 at 08:56