UITextView изменения contentSize и NSLayoutManager в iOS7

Проблема: UITextView в некоторых ситуациях молча меняет свое contentSize.

Самый простой корпус textView с крупным текстом и клавиатурой. Просто добавьте выход UITextView и установите - 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}

Похоже, поведение UITextView сильно изменилось в iOS7. И некоторые вещи сейчас сломаны.

Узнав больше, я обнаружил, что новое свойство layoutManager моего textView также меняется. Теперь в логе есть интересная информация:

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?


person zxcat    schedule 16.09.2013    source источник
comment
У меня точно такая же проблема :(   -  person Legolas    schedule 17.09.2013


Ответы (3)


Похоже, проблема в layoutManager по умолчанию 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, подробности см. в вопросе.

Для получения дополнительной информации см.:

person zxcat    schedule 19.09.2013
comment
К сожалению, это не работает для меня. Я делаю, как сказано здесь, только с пользовательским хранилищем текста, а contentSize все еще изменяется при прокрутке представления. - person Joshua; 14.10.2013
comment
Кажется, причина, по которой это работает, заключается в том, что вы установили высоту текстового контейнера, ограниченную высотой представления. Это не очень хорошо для диспетчера компоновки при вызове других методов, таких как lineFragmentUsedRectForGlyphAtIndex, поскольку он просто игнорирует все, что выходит за пределы размера текстового контейнера. - person Joshua; 18.10.2013
comment
После некоторых исследований кажется, что прыжок происходит, если высота текстового контейнера выше 9999999, поэтому все, что выше и включая 10 миллионов (10 000 000), вызывает прыжок. Я не уверен, почему этот номер особенный, но, похоже, так оно и есть. - person Joshua; 18.10.2013
comment
Это отлично сработало для меня. Не знаю, почему это работает, но я оказался здесь, следуя той же логике, но простая замена текстового контейнера по умолчанию на тот, который был создан здесь, решила проблему. - person user1043479; 29.01.2014
comment
@zxcat здесь немного не по теме, но все же: NSLayoutManager дает возможность иметь в нем более одного NSTextContainer. Как мы можем применить менеджер компоновки с несколькими текстовыми контейнерами к UITextView, который может добавить только один NSTextContainer? - person Dima Deplov; 10.07.2016
comment
@flinth, я не могу ответить на твой вопрос. Я использовал NSLayoutManager только как обходной путь для ошибки iOS7 и никогда не исследовал его глубоко. (На самом деле эта ошибка исправлена ​​в iOS8 или 9) - person zxcat; 18.08.2016

Я встречал аналогичную ситуацию, как urs. Мой показывает другую ошибку, но по той же причине: свойство 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. Надеюсь, Apple скоро это исправит. Надеюсь, это полезно.

person long long    schedule 01.10.2013
comment
Спасибо за Ваш ответ. Но сам я не использую contentSize. UITextView использует его (изменения размера содержимого вызывают скачки текста в UITextView). Поэтому мне нужно предотвратить изменения, а не получать правильное значение contentSize при чтении. - person zxcat; 01.10.2013
comment
Другой обходной путь, который я пробовал, заключался в переопределении setContentSize: и не изменении значения, если выполняется разметка. Но это действительно грязный хак. Итак, теперь я использую обходной путь с созданием и присоединением NSLayoutManager к UITextView вместо использования встроенного менеджера компоновки. Плохая сторона: такие UITextView нельзя поместить в раскадровку/xib - person zxcat; 01.10.2013
comment
@zxcat, я попробовал ваш обходной путь layoutManger. Это работает и для меня. Но есть вещь, которую я не понимаю. _textView = [[MyTextView alloc] initWithFrame:..... Он не вызывает initWithFrame в классе MyTextView. Я должен выполнить первоначальную работу в viewDidLoad. Это почему? - person long long; 01.10.2013
comment
1. MyTextView не нужен, он был частью вопроса. Вы можете использовать обычный UITextView. 2. Если у вас есть собственный подкласс UITextView, обязательно переопределите -initWithFrame:textContainer:, а не простой initWithFrame: - person zxcat; 01.10.2013
comment
Ах, ты прав. Мои близорукие глаза не видели второй параметр textContainer! - person long long; 01.10.2013

Кажется, ошибка в 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];
    }];
}
person torap    schedule 23.10.2013
comment
Спасибо за исправление! Мое текстовое представление теперь снова хорошо прокручивается при редактировании и находится в последней отображаемой строке! - person nicolas; 15.01.2014