Вертикально выравнивать текст UILabel с ограничениями и без обертывания (автоматический макет, одна строка)

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

enter image description here

Однако, как мы знаем, вы не можете вертикально выравнивать текст в UILabel. В моем тексте обновляется размер шрифта в зависимости от длины содержимого. Полноразмерный текст выглядит великолепно, в то время как небольшой текст значительно ниже на экране.

enter image description here

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

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

Поэтому мне осталось искать альтернативные решения. Насколько я вижу, вам может потребоваться установить ограничение высоты на метке и отрегулировать константу высоты после вычисления высоты текста. У кого-нибудь есть хорошее решение?

Ответы

Ответ 1

Эта проблема - реальная задача PITA. Это не помогает, что API, который работает, устарел в iOS7 или что API замены iOS7 нарушен. Мля!

Ваше решение хорошо, однако оно использует устаревший API (sizeWithFont:minFontSize:actualFontSize:forWidth:lineBreakMode:), и он не очень хорошо инкапсулирован - вам нужно скопировать этот код в любые ячейки или представления, где вы хотите этого поведения. С положительной стороны это довольно эффективно! Одна ошибка может заключаться в том, что метка еще не была выложена, когда вы выполняете расчет, но вы выполняете расчет на основе его ширины.

Я предлагаю вам инкапсулировать это поведение в подкласс UILabel. Поместив вычисление размера в переопределенном методе intrinsicContentSize, метка будет автоматически настраиваться. Я написал следующее, которое включает ваш код, который будет выполняться на iOS6, и мою версию, используя не устаревший API для iOS7 или лучше:

@implementation TSAutoHeightLabel

- (CGSize) intrinsicContentSize
{
    NSAssert( self.baselineAdjustment == UIBaselineAdjustmentAlignCenters, @"Please ensure you are using UIBaselineAdjustmentAlignCenters!" );

    NSAssert( self.numberOfLines == 1, @"This is only for single-line labels!" );

    CGSize intrinsicContentSize;

    if ( [self.text respondsToSelector: @selector( boundingRectWithSize:options:attributes:context: )] )
    {
        NSStringDrawingContext* context = [NSStringDrawingContext new];
        context.minimumScaleFactor = self.minimumScaleFactor;

        CGSize inaccurateSize = [self.text boundingRectWithSize: CGSizeMake( self.bounds.size.width, CGFLOAT_MAX )
                                                        options: NSStringDrawingUsesLineFragmentOrigin
                                                     attributes: @{ NSFontAttributeName : self.font }
                                                        context: context].size;

        CGSize accurateSize = [self.text sizeWithAttributes: @{ NSFontAttributeName : [UIFont fontWithName: self.font.fontName size: 12.0] } ];

        CGFloat accurateHeight = accurateSize.height * inaccurateSize.width / accurateSize.width;

        intrinsicContentSize = CGSizeMake( inaccurateSize.width, accurateHeight);
    }
    else
    {
        CGFloat actualFontSize;

#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"

        [self.text sizeWithFont: self.font
                    minFontSize: self.minimumFontSize
                 actualFontSize: &actualFontSize
                       forWidth: self.frame.size.width
                  lineBreakMode: NSLineBreakByTruncatingTail];

#pragma GCC diagnostic pop

        CGRect lineBox = CTFontGetBoundingBox((__bridge CTFontRef)([UIFont fontWithName: self.font.fontName size: actualFontSize]));

        intrinsicContentSize = lineBox.size;
    }

    return intrinsicContentSize;
}

@end

Эта реализация не идеальна. Я должен был обеспечить использование baselineAdjustment == UIBaselineAdjustmentAlignCenters, и я не уверен на 100%, я понимаю, почему. И я не доволен обручами, которые мне пришлось перепрыгнуть, чтобы получить точную высоту текста. Там также разница в пикселях между тем, что производит мой расчет, и вашим. Не стесняйтесь играть с ним и при необходимости настраивайте:)

API boundingRectWithSize:options:attributes:context кажется мне сломанным. Хотя он (в основном!) Правильно сжимает текст до размера ввода, он не вычисляет правильную высоту! Высота, которую он возвращает, основана на высоте строки поставляемого шрифта, даже если масштабирование находится в игре. Мое предположение заключается в том, почему UILabel не имеет такого поведения по умолчанию? Моим обходным путем является вычисление неограниченного размера, где высота и ширина являются точными, а затем используйте соотношение между ограниченной и неограниченной шириной для вычисления точной высоты для ограниченного размера. Что такое ПИТА. На форумах Apple dev есть много жалоб и здесь, на SO, которые указывают, что этот API имеет ряд проблем, подобных этому.

Ответ 2

Итак, я нашел обходное решение. Это немного рискованно, но он работает.

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

Затем в layoutSubviews я обновляю свою высоту ограничения в зависимости от размера шрифта, который я должен вычислить:

- (void)layoutSubviews {
    if (self.titleLabel.text) {
        CGFloat actualFontSize;
        CGSize titleSize = [self.titleLabel.text sizeWithFont:self.titleLabel.font minFontSize:9.0 actualFontSize:&actualFontSize forWidth:self.titleLabel.frame.size.width lineBreakMode:NSLineBreakByTruncatingTail];
        CGRect lineBox = CTFontGetBoundingBox((__bridge CTFontRef)([UIFont fontWithName:@"ProximaNova-Regular" size:actualFontSize]));
        self.titleHeightConstraint.constant = lineBox.size.height;
    }
    [super layoutSubviews];
}

Сначала я просто установил его на фактический размер шрифта, но даже с настройкой (* 1.2) он все еще обрезал меньшие размеры шрифта. Ключ использовал CTFontGetBoundingBox с размером шрифта, определенным по моим расчетам.

Это довольно неудачно, и я надеюсь, что там лучший способ. Возможно, я должен переключиться на обертку.

Ответ 3

TomSwift благодарит за ваш ответ, я действительно боролся с этой проблемой.

Если кто-то все еще получает странное поведение, мне пришлось изменить:

intrinsicContentSize = CGSizeMake (inaccurateSize.width, exactHeight);

к

intrinsicContentSize = CGSizeMake (inaccurateSize.width, exactHeight * 2);

тогда он работал как прелесть.

Ответ 4

То, что вы ищете, это две строки кода.

myLabel.numberOfLines = 0;
myLabel.lineBreakMode = UILineBreakModeWordWrap;

и вы также найдете это в Инспекторе атрибутов в разделе "Разрывы строк" ​​и "Линии".