Ошибка вычисления высоты строки AutoLayout для NSAttributedString

Мое приложение вытаскивает HTML из API, преобразует его в NSAttributedString (чтобы разрешать связанные ссылки) и записывает его в строку в таблице AutoLayout. Проблема в том, что всякий раз, когда я вызываю этот тип ячейки, высота просчитывается и содержимое обрезается. Я пробовал различные реализации вычислений высоты строки, ни одна из которых не работает правильно.

Как я могу и динамически вычислять высоту одной из этих строк, сохраняя при этом возможность использовать ссылки HTML?

Пример нежелательного поведения

Мой код ниже.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    switch(indexPath.section) {
        ...
        case kContent:
        {
            FlexibleTextViewTableViewCell* cell = (FlexibleTextViewTableViewCell*)[TableFactory getCellForIdentifier:@"content" cellClass:FlexibleTextViewTableViewCell.class forTable:tableView withStyle:UITableViewCellStyleDefault];

            [self configureContentCellForIndexPath:cell atIndexPath:indexPath];
            [cell.contentView setNeedsLayout];
            [cell.contentView layoutIfNeeded];
            cell.selectionStyle = UITableViewCellSelectionStyleNone;
            cell.desc.font = [UIFont fontWithName:[StringFactory defaultFontType] size:14.0f];

            return cell;
        }
        ...
        default:
            return nil;
    }
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    UIFont *contentFont = [UIFont fontWithName:[StringFactory defaultFontType] size:14.0f];
    switch(indexPath.section) {
        ...
        case kContent:
            return [self textViewHeightForAttributedText:[self convertHTMLtoAttributedString:myHTMLString] andFont:contentFont andWidth:self.tappableCell.width];
            break;
        ...
        default:
            return 0.0f;
    }
}

-(NSAttributedString*) convertHTMLtoAttributedString: (NSString *) html {
    return [[NSAttributedString alloc] initWithData:[html dataUsingEncoding:NSUTF8StringEncoding]
                                            options:@{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
                                                      NSCharacterEncodingDocumentAttribute: @(NSUTF8StringEncoding)}
                                 documentAttributes:nil
                                              error:nil];
}

- (CGFloat)textViewHeightForAttributedText:(NSAttributedString*)text andFont:(UIFont *)font andWidth:(CGFloat)width {
    NSMutableAttributedString *mutableText = [[NSMutableAttributedString alloc] initWithAttributedString:text];

    [mutableText addAttribute:NSFontAttributeName value:font range:NSMakeRange(0, text.length)];

    UITextView *calculationView = [[UITextView alloc] init];
    [calculationView setAttributedText:mutableText];

    CGSize size = [self text:mutableText.string sizeWithFont:font constrainedToSize:CGSizeMake(width,FLT_MAX)];
    CGSize sizeThatFits = [calculationView sizeThatFits:CGSizeMake(width, FLT_MAX)];

    return sizeThatFits.height;
}

Ответ 1

В приложении, над которым я работаю, приложение вытягивает ужасные строки HTML из паршивого API, написанного другими людьми, и преобразует строки HTML в объекты NSAttributedString. У меня нет выбора, кроме как использовать этот паршивый API. Очень грустный. Любой, кто должен разбирать ужасную строку HTML, знает мою боль. Я использую Text Kit. Вот как:

  • parse html string, чтобы получить объект DOM. Я использую libxml со световой оболочкой, hpple. Эта комбинация очень проста и проста в использовании. Настоятельно рекомендуется.
  • рекурсивно перемещайте объект DOM для создания объекта NSAttributedString, используйте специальный атрибут для маркировки ссылок, используйте NSTextAttachment для пометки изображений. Я называю это богатым текстом.
  • создавать или повторно использовать первичные объекты Text Kit. т.е. NSLayoutManager, NSTextStorage, NSTextContainer. Подключите их после выделения. Процесс компоновки
    • Передайте богатый текст, построенный на шаге 2, на объект NSTextStorage на шаге 3. с помощью [NSTextStorage setAttributedString:]
    • использовать метод [NSLayoutManager ensureLayoutForTextContainer:], чтобы заставить макет произойти.
  • вычислить кадр, необходимый для рисования богатого текста с помощью метода [NSLayoutManager usedRectForTextContainer:]. При необходимости добавьте отступ или маржу.
  • процесс рендеринга
    • вернуть высоту, вычисленную на шаге 5, в [tableView: heightForRowAtIndexPath:]
    • нарисуйте богатый текст на шаге 2 с помощью [NSLayoutManager drawGlyphsForGlyphRange:atPoint:]. Я использую внеэкранную технику рисования здесь, поэтому результатом является объект UIImage.
    • используйте UIImageView для рендеринга конечного изображения. Или передать объект изображения результата в свойство contents свойства layer свойства contentView объекта UITableViewCell в [tableView:cellForRowAtIndexPath:].
  • обработка событий
    • захватить событие касания. Я использую признак распознавания жесткого диска, прикрепленный к представлению таблицы.
    • получить местоположение события касания. Используйте это местоположение, чтобы проверить, что пользователь нажал ссылку или изображение с помощью [NSLayoutManager glyphIndexForPoint:inTextContainer:fractionOfDistanceThroughGlyph] и [NSAttributedString attribute:atIndex:effectiveRange:].

Фрагмент кода обработки событий:

CGPoint location = [tap locationInView:self.tableView];
// tap is a tap gesture recognizer

NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint:location];
if (!indexPath) {
    return;
}

CustomDataModel *post = [self getPostWithIndexPath:indexPath];
// CustomDataModel is a subclass of NSObject class.

UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
location = [tap locationInView:cell.contentView];
// the rich text is drawn into a bitmap context and rendered with 
// cell.contentView.layer.contents

// The `Text Kit` objects can  be accessed with the model object.
NSUInteger index = [post.layoutManager 
                        glyphIndexForPoint:location 
                           inTextContainer:post.textContainer 
            fractionOfDistanceThroughGlyph:NULL];

CustomLinkAttribute *link = [post.content.richText 
                                 attribute:CustomLinkAttributeName 
                                   atIndex:index 
                            effectiveRange:NULL];
// CustomLinkAttributeName is a string constant defined in other file
// CustomLinkAttribute is a subclass of NSObject class. The instance of 
// this class contains information of a link
if (link) {
    // handle tap on link
}

// same technique can be used to handle tap on image

Этот подход намного быстрее и настраивается, чем [NSAttributedString initWithData:options:documentAttributes:error:] при рендеринге той же строки html. Даже без профилирования я могу сказать, что подход Text Kit выполняется быстрее. Это очень быстро и удовлетворительно, хотя мне приходится разбирать html и самостоятельно строить атрибутную строку. Подход NSDocumentTypeDocumentAttribute слишком медленный, поэтому неприемлем. С помощью Text Kit я также могу создать сложный макет, такой как текстовый блок с переменным отступом, границей, вложенным текстовым блоком с глубиной и т.д. Но ему нужно написать больше кода для построения NSAttributedString и управлять процессом макета. Я не знаю, как вычислить ограничивающий прямоугольник атрибутной строки, созданной с помощью NSDocumentTypeDocumentAttribute. Я считаю, что привязанные строки, созданные с помощью NSDocumentTypeDocumentAttribute, обрабатываются Web Kit вместо Text Kit. Таким образом, это не предназначено для ячеек просмотра переменной высоты таблицы.

EDIT: Если вы должны использовать NSDocumentTypeDocumentAttribute, я думаю, вам нужно выяснить, как происходит процесс компоновки. Возможно, вы можете установить некоторые точки останова, чтобы увидеть, какой объект отвечает за процесс компоновки. Тогда, возможно, вы можете запросить этот объект или использовать другой подход для имитации процесса компоновки, чтобы получить информацию о макете. Некоторые люди используют ad-hoc-ячейку или объект UITextView для вычисления высоты, которая, по-моему, не является хорошим решением. Потому что таким образом, приложение должно составлять один и тот же фрагмент текста, по крайней мере, дважды. Знаете ли вы или нет, где-то в вашем приложении, какой-то объект должен разложить текст так, чтобы вы могли получить информацию о макете как ограничивающий прямоугольник. Поскольку вы упомянули класс NSAttributedString, лучшим решением является Text Kit после iOS 7. Или Core Text, если ваше приложение нацелено на более раннюю версию iOS.

Я настоятельно рекомендую Text Kit, потому что таким образом, для каждой строки html, выведенной из API, процесс компоновки происходит только один раз, а информация о макете, такая как ограничивающий прямоугольник и позиции каждого глифа, кэшируется объектом NSLayoutManager. Пока объекты Text Kit сохраняются, вы всегда можете их повторно использовать. Это чрезвычайно эффективно при использовании представления таблицы для визуализации текста произвольной длины, поскольку текст выкладывается только один раз и рисуется каждый раз, когда ячейка необходима для отображения. Я также рекомендую использовать Text Kit без UITextView, как предполагалось в официальных документах Apple. Потому что нужно кэшировать каждый UITextView, если он хочет повторно использовать объекты Text Kit, прикрепленные к этому UITextView. Прикрепите объекты Text Kit для моделирования таких объектов, как я, и обновляем NSTextStorage и принудительно NSLayoutManager для компоновки, когда из HTML вытаскивается новая строка html. Если число строк табличного представления фиксировано, можно также использовать фиксированный список объектов модели-заполнителя, чтобы избежать повторного распределения и конфигурации. И поскольку drawRect: заставляет Core Animation создавать бесполезные растровые изображения, которые следует избегать, не используйте UIView и drawRect:. Либо используйте технику рисования CALayer, либо нарисуйте текст в растровом контексте. Я использую последний подход, потому что это можно сделать в фоновом потоке с GCD, поэтому основной поток может реагировать на работу пользователя. Результат в моем приложении действительно удовлетворительный, он быстрый, набор верстки хорош, прокрутка табличного представления очень плавная (60 кадров в секунду), поскольку весь процесс рисования выполняется в фоновом потоке с помощью GCD. Каждое приложение должно нарисовать текст с табличным представлением, используя Text Kit.

Ответ 2

Я предполагаю, что вы используете UILabel для отображения строки?

Если это так, у меня было много проблем с многострочными метками с автозапуском. Я дал здесь ответ

Автозапуск таблицы сокетов в iOS8

который также ссылается на другой мой ответ, в котором есть разбивка того, как я решил все мои проблемы. Подобные проблемы снова возникли в iOS 8, для которых требуется аналогичное исправление в другой области.

Все сводится к идее установки UILabel preferredMaxLayoutWidth каждый раз, когда изменяется граница. Также помогло установить ширину ячеек ширины таблицы перед запуском:

CGSize size = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];

Ответ 3

Я столкнулся с очень похожими проблемами в другом проекте, где поля, использующие NSAttributedString, не отображались с правильной высотой. К сожалению, есть две ошибки, которые заставили нас полностью отказаться от использования этого проекта.

Во-первых, это ошибка, которую вы заметили здесь, когда какой-то HTML вызывает неправильный расчет размера. Обычно это происходит из пространства между тэгами. Внедрение CSS-типа решило проблему, но мы не контролировали входящий формат. Это ведет себя по-разному между iOS7 и iOS8, где это неправильно на одном и праве на другом.

Вторая (и более серьезная) ошибка заключается в том, что NSAttributedString абсурдно медленна в iOS 8. Я изложил ее здесь: Производительность NSAttributedString хуже в iOS 8

Вместо того, чтобы создавать кучу хаков, чтобы все выполнялось так, как мы хотели, предложение об использовании https://github.com/Cocoanetics/DTCoreText получилось очень хорошо для проект.

Ответ 4

Вам необходимо обновить собственный размер содержимого.

Я предполагаю, что вы установили атрибутный текст для метки в этом коде [self configureContentCellForIndexPath:cell atIndexPath:indexPath];

Итак, это должно выглядеть так:

cell.youLabel.attributedText = NSAttributedString(...) 
cell.youLabel.invalidateIntrinsicContentSize()
cell.youLabel.layoutIfNeeded()

Код расчета высоты (CGFloat)textViewHeightForAttributedText:(NSAttributedString*)text andFont:(UIFont *)font andWidth:(CGFloat)width должен быть заменен вычислением высоты ячейки с использованием ячейки прототипа.

Ответ 5

Если вы можете настроить таргетинг на iOS 8, используя динамическое определение размеров ячеек, это идеальное решение вашей проблемы.

Чтобы использовать динамический размер ячейки, удалите heightForRowAtIndexPath: и установите self.tableView.rowHeight в UITableViewAutomaticDimension.

Вот видео с более подробной информацией: https://developer.apple.com/videos/wwdc/2014/?include=226#226

Ответ 6

Вы можете заменить этот метод, чтобы вычислить высоту присваиваемой строки:

- (CGFloat)textViewHeightForAttributedText:(NSAttributedString*)text andFont:(UIFont *)font andWidth:(CGFloat)width {
CGFloat result = font.pointSize + 4;
if (text)
    result = (ceilf(CGRectGetHeight([text boundingRectWithSize:CGSizeMake(width, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading context:nil])) + 1);

return result;

}

Возможно, шрифт, который вы изменили, не соответствует шрифту содержимого на страницах html. Итак, используйте этот метод для создания атрибутной строки с соответствующим шрифтом:

//HTML → NSAttributedString

-(NSAttributedString*) convertHTMLtoAttributedString: (NSString *) html {
NSError *error;
NSDictionary *options = @{NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType};
NSAttributedString *attrString = [[NSAttributedString alloc] initWithData:[html dataUsingEncoding:NSUTF8StringEncoding] options:options documentAttributes:nil error:&error];
if(!attrString) {
    NSLog(@"creating attributed string from HTML failed: %@", error.debugDescription);
}
return attrString;

}

//заставить шрифт thrugh и css

- (NSAttributedString *)attributedStringFromHTML:(NSString *)html withFont:(UIFont *)font    {
return [self convertHTMLtoAttributedString:[NSString stringWithFormat:@"<span style=\"font-family: %@; font-size: %f\";>%@</span>", font.fontName, font.pointSize, html]];

}

и в таблице table: heightForRowAtIndexPath: замените его на это:

case kContent:
        return [self textViewHeightForAttributedText:[self attributedStringFromHTML:myHTMLString withFont:contentFont] andFont:contentFont andWidth:self.tappableCell.width]; 
        break;

Ответ 7

Вы должны иметь возможность конвертировать в NSString для вычисления такой высоты.

-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UIFont * font = [UIFont systemFontOfSize:15.0f];
    NSString *text = [getYourAttributedTextArray objectAtIndex:indexPath.row] string];
    CGFloat height = [text boundingRectWithSize:CGSizeMake(self.tableView.frame.size.width, maxHeight) options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading) attributes:@{NSFontAttributeName: font} context:nil].size.height;

    return height + additionalHeightBuffer;
}

Ответ 8

[cell.descriptionLabel setPreferredMaxLayoutWidth: 375.0];