Многолинейный UILabel iOS AutoLayout

Следующий вопрос - это некоторое продолжение этого:

iOS: многострочный UILabel в автоматическом макете

Основная идея заключается в том, что каждый вид должен указывать его "предпочтительный" (внутренний) размер, чтобы AutoLayout мог знать, как правильно отображать его. UILabel - это всего лишь пример ситуации, когда представление не может самостоятельно знать, какой размер ему нужен для отображения. Это зависит от того, какая ширина предоставляется.

Как отмечалось в mwhuss, setPreferredMaxLayoutWidth сделал трюк, чтобы сделать ярлык на несколько строк. Но здесь не главный вопрос. Вопрос в том, где и когда я получаю это значение ширины, которое я отправляю в качестве аргумента для setPreferredMaxLayoutWidth.

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

В UIView

-(CGSize) intrinsicContentSize

Я устанавливаюPreferredMaxLayoutWidth для моих UILabels в соответствии с self.frame.width.

UIViewController-х

-(void) viewDidLayoutSubviews

- это первый метод обратного вызова, который я знаю, где субсвиды основного вида назначаются с их точными кадрами, которые они населяют на экране. Изнутри этого метода я работаю над своими подзонами, недействительными их внутренними размерами, чтобы UILabels разбивались на несколько строк на основе ширины, которая была им назначена.

Ответ 1

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

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

Итак, чтобы это произошло автоматически, я использовал подкласс UILabel, который переопределяет setBounds:. Здесь вызовите супер-реализацию, затем , если это уже не так,, установите максимальную ширину макета макета ширины границ.

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

Ответ 2

Ответ на этот вопрос в objc.io в разделе "Внутренний размер содержимого многострочного текста" Расширенные инструменты автоматической компоновки. Здесь соответствующая информация:

Внутренний размер содержимого UILabel и NSTextField неоднозначен для многострочного текста. Высота текста зависит от ширины линий, которая еще не определена при решении ограничений. Чтобы решить эту проблему, оба класса имеют новое свойство, называемое preferredMaxLayoutWidth, которое определяет максимальную ширину линии для вычисления внутреннего размера содержимого.

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

Код, который они дают для использования внутри контроллера вида:

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    myLabel.preferredMaxLayoutWidth = myLabel.frame.size.width;
    [self.view layoutIfNeeded];
}

Взгляните на их сообщение, там больше информации о том, зачем нужно делать макет дважды.

Ответ 3

Обновление

Мой оригинальный ответ оказался полезным, поэтому я оставил его нетронутым ниже, однако в моих собственных проектах я нашел более надежное решение, которое работает с ошибками в iOS 7 и iOS 8. https://github.com/nicksnyder/ios-cell-layout

Оригинальный ответ

Это полное решение, которое работает для меня на iOS 7 и iOS 8

Цель C

@implementation AutoLabel

- (void)setBounds:(CGRect)bounds {
  if (bounds.size.width != self.bounds.size.width) {
    [self setNeedsUpdateConstraints];
  }
  [super setBounds:bounds];
}

- (void)updateConstraints {
  if (self.preferredMaxLayoutWidth != self.bounds.size.width) {
    self.preferredMaxLayoutWidth = self.bounds.size.width;
  }
  [super updateConstraints];
}

@end

Swift

import Foundation

class EPKAutoLabel: UILabel {

    override var bounds: CGRect {
        didSet {
            if (bounds.size.width != oldValue.size.width) {
                self.setNeedsUpdateConstraints();
            }
        }
    }

    override func updateConstraints() {
        if(self.preferredMaxLayoutWidth != self.bounds.size.width) {
            self.preferredMaxLayoutWidth = self.bounds.size.width
        }
        super.updateConstraints()
    }
}

Ответ 4

У нас была ситуация, когда авто-макет UILabel внутри UIScrollView был изложен в портрете, но при повороте в альбомной ориентации высота UILabel не была пересчитана.

Мы обнаружили, что ответ от @jrturton зафиксировал это, по-видимому, потому, что теперь корректно задан предпочтительныйMaxLayoutWidth.

Вот код, который мы использовали. Просто установите класс Custom из конструктора интерфейса как CVFixedWidthMultiLineLabel.

CVFixedWidthMultiLineLabel.h

@interface CVFixedWidthMultiLineLabel : UILabel

@end 

CVFixedWidthMultiLineLabel.m

@implementation CVFixedWidthMultiLineLabel

// Fix for layout failure for multi-line text from
// http://stackoverflow.com/questions/17491376/ios-autolayout-multi-line-uilabel
- (void) setBounds:(CGRect)bounds {
    [super setBounds:bounds];

    if (bounds.size.width != self.preferredMaxLayoutWidth) {
        self.preferredMaxLayoutWidth = self.bounds.size.width;
    }
}

@end

Ответ 5

Поскольку мне не разрешено добавлять комментарий, я обязан добавить его в качестве ответа. Версия jrturton работала только для меня, если я вызываю layoutIfNeeded в updateViewConstraints перед тем, как получить preferredMaxLayoutWidth соответствующей метки. Без вызова layoutIfNeeded preferredMaxLayoutWidth всегда 0 в updateViewConstraints. И все же, всегда было нужное значение при проверке в setBounds:. Мне не удалось узнать, КОГДА был установлен правильный preferredMaxLayoutWidth. Я переопределяю setPreferredMaxLayoutWidth: в подклассе UILabel, но он никогда не вызывался.

Обобщенный, I:

  • ... sublcassed UILabel
  • ... и переопределить setBounds: to, , если он еще не установлен, установите preferredMaxLayoutWidth в CGRectGetWidth(bounds)
  • ... вызов [super updateViewConstraints] перед следующим
  • ... вызов layoutIfNeeded, прежде чем использовать preferredMaxLayoutWidth для расчета размера метки

РЕДАКТИРОВАТЬ: Этот способ обхода работы, по-видимому, иногда работает или может понадобиться. У меня просто была проблема (iOS 7/8), где высота метки была неправильно рассчитана, поскольку preferredMaxLayoutWidth вернулся 0 после того, как процесс компоновки был выполнен один раз. Поэтому после некоторых проб и ошибок (и обнаружив эту запись в блоге), я снова переключился на использование UILabel и просто установил верхний, нижний, левый и правильные ограничения автоматической компоновки. И по какой-либо причине высота надписи была правильно установлена ​​после обновления текста.

Ответ 6

Как было предложено другим ответом, я попытался переопределить viewDidLayoutSubviews:

- (void)viewDidLayoutSubviews
{
    [super viewDidLayoutSubviews];
    _subtitleLabel.preferredMaxLayoutWidth = self.view.bounds.size.width - 40;
    [self.view layoutIfNeeded];
}

Это сработало, но оно было видимым в пользовательском интерфейсе и вызвало "видимое мерцание", то есть сначала метка была визуализирована с высотой двух строк, а затем была перерисована с высотой только одной строки.

Это было неприемлемо для меня.

Я нашел тогда лучшее решение, переопределив updateViewConstraints:

-(void)updateViewConstraints {
    [super updateViewConstraints];

    // Multiline-Labels and Autolayout do not work well together:
    // In landscape mode the width is still "portrait" when the label determines the count of lines
    // Therefore the preferredMaxLayoutWidth must be set
    _subtitleLabel.preferredMaxLayoutWidth = self.view.bounds.size.width - 40;
}

Это было лучшее решение для меня, потому что оно не вызывало "визуального мерцания".

Ответ 7

Использование boundingRectWithSize

Я разрешил свою борьбу с двумя многострочными метками в устаревшем UITableViewCell, который использовал "\n" в качестве разрыва строки, измеряя требуемую ширину следующим образом:

- (CGFloat)preferredMaxLayoutWidthForLabel:(UILabel *)label
{
    CGFloat preferredMaxLayoutWidth = 0.0f;
    NSString *text = label.text;
    UIFont *font = label.font;
    if (font != nil) {
        NSMutableParagraphStyle *mutableParagraphStyle = [[NSMutableParagraphStyle alloc] init];
        mutableParagraphStyle.lineBreakMode = NSLineBreakByWordWrapping;

        NSDictionary *attributes = @{NSFontAttributeName: font,
                                     NSParagraphStyleAttributeName: [mutableParagraphStyle copy]};
        CGRect boundingRect = [text boundingRectWithSize:CGSizeZero options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil];
        preferredMaxLayoutWidth = ceilf(boundingRect.size.width);

        NSLog(@"Preferred max layout width for %@ is %0.0f", text, preferredMaxLayoutWidth);
    }


    return preferredMaxLayoutWidth;
}

Тогда вызов метода был таким же простым, как:

CGFloat labelPreferredWidth = [self preferredMaxLayoutWidthForLabel:textLabel];
if (labelPreferredWidth > 0.0f) {
    textLabel.preferredMaxLayoutWidth = labelPreferredWidth;
}
[textLabel layoutIfNeeded];

Ответ 8

Чистым решением является установка rowcount = 0 и использование свойства для ограничения высоты вашего ярлыка. Затем после того, как контент будет установлен, вызов

CGSize sizeThatFitsLabel = [_subtitleLabel sizeThatFits:CGSizeMake(_subtitleLabel.frame.size.width, MAXFLOAT)];
_subtitleLabelHeightConstraint.constant = ceilf(sizeThatFitsLabel.height);

-(void) updateViewConstraints имеет проблему, так как iOS 7.1.

Ответ 9

В iOS 8 вы можете исправить проблемы компоновки многострочных меток в ячейке, вызвав cell.layoutIfNeeded() после удаления и настройки ячейки. Звонок безвреден в iOS 9.

См. ответ Ника Снайдера. Это решение было взято из его кода в https://github.com/nicksnyder/ios-cell-layout/blob/master/CellLayout/TableViewController.swift.