Высота динамических ячеек UITableView корректируется только после некоторой прокрутки


117

У меня есть UITableViewпользовательский, UITableViewCellопределенный в раскадровке с использованием автоматического макета. Ячейка имеет несколько многострочных UILabels.

В по- UITableViewвидимому, правильно CALCulate высоты клеток, но в течение первых нескольких клеток , что высота не должным образом разделена между этикетками. После небольшой прокрутки все работает как положено (даже ячейки, которые изначально были неверными).

- (void)viewDidLoad {
    [super viewDidLoad]
    // ...
    self.tableView.rowHeight = UITableViewAutomaticDimension;
}


- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    TableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"TestCell"];
    // ...
    // Set label.text for variable length string.
    return cell;
}

Есть ли что-то, чего мне может не хватать, из-за чего автоматический макет не может выполнять свою работу первые несколько раз?

Я создал образец проекта, демонстрирующий это поведение.

Образец проекта: вид сверху в виде таблицы из образца проекта при первой загрузке. Пример проекта: те же ячейки после прокрутки вниз и вверх.


1
Я также столкнулся с этой проблемой, но приведенный ниже ответ не работает для меня. Любая помощь по этому
поводу

1
столкнулся с той же проблемой, добавление layoutIfNeeded для ячейки не работает и у меня? любые другие предложения
Макс

1
Отличное место. Это все еще проблема в 2019 году: /
Fattie

Я нашел то, что мне помогло, в следующей ссылке - это довольно подробное обсуждение ячеек представления таблицы переменной высоты: stackoverflow.com/a/18746930/826946
Энди Вайнштейн,

Ответы:


139

Я не знаю, четко это задокументировано или нет, но добавление [cell layoutIfNeeded]ячейки перед возвратом решает вашу проблему.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    TableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"TestCell"];
    NSUInteger n1 = firstLabelWordCount[indexPath.row];
    NSUInteger n2 = secondLabelWordCount[indexPath.row];
    [cell setNumberOfWordsForFirstLabel:n1 secondLabel:n2];

    [cell layoutIfNeeded]; // <- added

    return cell;
}

3
Я действительно задаюсь вопросом, почему это необходимо только в первый раз? Это сработает так же хорошо, если я поставлю вызов -layoutIfNeededв ячейку -awakeFromNib. Я бы предпочел звонить только тогда, layoutIfNeededкогда знаю, зачем это нужно.
blackp

2
Сработало у меня! И я хотел бы знать, зачем это нужно!
Мустафа

Не сработало, если визуализировать класс размера, отличный от любого X любого
Андрей Константинов

@AndreyKonstantinov Да, это не работает с классом размера, как мне заставить его работать с классом размера?
z22

Он работает без размерного класса, любое решение с размерным классом?
Yuvrajsinh 02

38

Это сработало для меня, когда другие подобные решения не работали:

override func didMoveToSuperview() {
    super.didMoveToSuperview()
    layoutIfNeeded()
}

Это похоже на настоящую ошибку, поскольку я очень хорошо знаком с AutoLayout и тем, как использовать UITableViewAutomaticDimension, однако я все еще иногда сталкиваюсь с этой проблемой. Я рад, что наконец нашел что-то, что работает как обходной путь.


1
Лучше, чем принятый ответ, поскольку он вызывается только тогда, когда ячейка загружается из xib вместо отображения каждой ячейки. Гораздо меньше строк кода.
Роберт Вагстафф

3
Не забудьте позвонитьsuper.didMoveToSuperview()
cicerocamargo

@cicerocamargo Apple говорит о didMoveToSuperview: «Реализация этого метода по умолчанию ничего не делает».
Иван Сметанин

4
@IvanSmetanin не имеет значения, если реализация по умолчанию ничего не делает, вы все равно должны вызывать супер-метод, так как он действительно может что-то сделать в будущем.
TNguyen

(Просто между прочим, вы должны ОБЯЗАТЕЛЬНО назвать super. По этому поводу. Я никогда не видел проекта, в котором в целом команда не подклассифицирует более одного подкласса ячеек, поэтому, конечно, вы должны быть уверены, что выбрали их все. И как отдельный вопрос, что сказал TNguyen.)
Fattie

22

Добавление [cell layoutIfNeeded]в cellForRowAtIndexPathне работает для клеток, которые первоначально прокручивается вне зрения.

И не делает это в начале [cell setNeedsLayout].

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

Это довольно неприятно, поскольку у большинства разработчиков корректно работают Dynamic Type, AutoLayout и Self-Sizing Cells - за исключением этого раздражающего случая. Эта ошибка затрагивает все мои "более высокие" контроллеры табличного представления.


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

Спасибо @ ashy_32bit, это тоже решило эту проблему для меня.
ImpurestClub

это может показаться простой ошибкой, @Scenario не так ли? ужасные вещи.
Fattie

3
Использование [cell layoutSubviews]вместо layoutIfNeeded могло бы стать возможным исправлением. Обратитесь к stackoverflow.com/a/33515872/1474113
ypresto

14

У меня был такой же опыт в одном из моих проектов.

Почему это происходит?

Ячейка разработана в Раскадровке с некоторой шириной для какого-то устройства. Например 400 пикселей. Например, ваша этикетка имеет одинаковую ширину. Когда он загружается из раскадровки, он имеет ширину 400 пикселей.

Вот проблема:

tableView:heightForRowAtIndexPath: вызывается перед макетом ячейки, это подвиды.

Таким образом, он рассчитал высоту для метки и ячейки шириной 400 пикселей. Но вы работаете на устройстве с экраном, например, 320 пикселей. И эта автоматически рассчитанная высота неверна. Просто потому, что ячейка layoutSubviewsпроисходит только после того, как tableView:heightForRowAtIndexPath: даже если вы установите preferredMaxLayoutWidthдля своей метки вручную, layoutSubviewsэто не помогает.

Мое решение:

1) Подкласс UITableViewи переопределение dequeueReusableCellWithIdentifier:forIndexPath:. Установите ширину ячейки равной ширине таблицы и задайте макет ячейки.

- (UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [super dequeueReusableCellWithIdentifier:identifier forIndexPath:indexPath];
    CGRect cellFrame = cell.frame;
    cellFrame.size.width = self.frame.size.width;
    cell.frame = cellFrame;
    [cell layoutIfNeeded];
    return cell;
}

2) Подкласс UITableViewCell. Установите preferredMaxLayoutWidthвручную для ваших этикеток в формате layoutSubviews. Также вам нужна ручная компоновка contentView, потому что она не компоновка автоматически после изменения кадра ячейки (я не знаю почему, но это так)

- (void)layoutSubviews {
    [super layoutSubviews];
    [self.contentView layoutIfNeeded];
    self.yourLongTextLabel.preferredMaxLayoutWidth = self.yourLongTextLabel.width;
}

1
Когда я столкнулся с этой проблемой, мне также нужно было убедиться, что фрейм табличного представления был правильным, поэтому я вызвал [self.tableview layoutIfNeeded] перед заполнением табличного представления (в viewDidLoad).
GK100

Я никогда не переставал думать, что это связано с неправильной шириной. cell.frame.size.width = tableview.frame.widthто cell.layoutIfNeeded()в cellForRowAtфункции сделал трюк для меня
Cam Connor

10

У меня аналогичная проблема, при первой загрузке высота строки не рассчитывалась, но после некоторой прокрутки или перехода на другой экран, и я возвращаюсь к этому экрану, строки вычисляются. При первой загрузке мои элементы загружаются из Интернета, а при второй загрузке мои элементы загружаются сначала из Core Data и перезагружаются из Интернета, и я заметил, что высота строк рассчитывается при перезагрузке из Интернета. Итак, я заметил, что когда tableView.reloadData () вызывается во время анимации перехода (такая же проблема с push и текущим переходом), высота строки не вычисляется. Поэтому я спрятал tableview при инициализации представления и поставил загрузчик активности, чтобы предотвратить неприятный эффект для пользователя, и я вызываю tableView.reloadData через 300 мс, и теперь проблема решена. Я думаю, что это ошибка UIKit, но этот обходной путь поможет.

Я помещаю эти строки (Swift 3.0) в обработчик завершения загрузки элемента

DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300), execute: {
        self.tableView.isHidden = false
        self.loader.stopAnimating()
        self.tableView.reloadData()
    })

Это объясняет, почему некоторые люди помещают reloadData в layoutSubviews, чтобы решить проблему.


Это единственный WA, который у меня тоже работал. За исключением того, что я не использую isHidden, но устанавливаю альфа таблицы на 0 при добавлении источника данных, а затем на 1 после перезагрузки.
PJ_Finnegan

6

Ни одно из вышеперечисленных решений у меня не сработало, сработал этот рецепт волшебства: назовите их в таком порядке:

tableView.reloadData()
tableView.layoutIfNeeded() tableView.beginUpdates() tableView.endUpdates()

мои данные tableView заполняются из веб-службы, при обратном вызове соединения я пишу приведенные выше строки.


1
Для Swift 5 это сработало даже в режиме коллекции, да благословит вас Бог, братан.
Говани Дхрув Виджайкумар,

Для меня это вернуло правильную высоту и расположение ячейки, но в нем не было данных
Дарроу Хартман

Попробуйте setNeedsDisplay () после этих строк
JAHelia

5

В моем случае последняя строка UILabel была усечена, когда ячейка отображалась в первый раз. Это произошло довольно случайно, и единственный способ правильно определить размер - вывести ячейку из поля зрения и вернуть ее обратно. Я пробовал все возможные решения, представленные до сих пор (layoutIfNeeded..reloadData), но у меня ничего не сработало. Хитрость заключалась в наборе «Autoshrink» в Minimuum шрифта Scale (0.5 для меня). Попробуйте


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

2
Но это автоматически сжимает текст ... зачем нужно иметь разные размеры текста в табличном представлении? Выглядит ужасно.
lucius degeer

5

Добавьте ограничение для всего содержимого в пользовательской ячейке табличного представления, затем оцените высоту строки табличного представления и установите для высоты строки автоматический размер с загрузкой viewdid:

    override func viewDidLoad() {
    super.viewDidLoad()

    tableView.estimatedRowHeight = 70
    tableView.rowHeight = UITableViewAutomaticDimension
}

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

class CustomTableViewCell: UITableViewCell {

override func awakeFromNib() {
    super.awakeFromNib()
    self.layoutIfNeeded()
    // Initialization code
}
}

Важно установить estimatedRowHeightзначение> 0, а не UITableViewAutomaticDimension(то есть -1), иначе автоматическая высота строки не будет работать.
PJ_Finnegan

5

Я попробовал ответить на большинство ответов на этот вопрос и не смог заставить работать ни один из них. Единственное функциональное решение, которое я нашел, - это добавить к моему UITableViewControllerподклассу следующее:

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    UIView.performWithoutAnimation {
        tableView.beginUpdates()
        tableView.endUpdates()
    }
}

UIView.performWithoutAnimationТребуется вызов, в противном случае вы увидите обычную таблицу анимации нагрузок вида контроллера.


работает и определенно лучше, чем дважды вызывать reloadData
Джимми Джордж Томас

2
Черная магия. Делать это в viewWillAppearне работает для меня, но делать это в viewDidAppearсделал.
Мартин

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

удивительно, я пробовал много примеров, но потерпел неудачу, ты демон,
Шакил Ахмед

То же, что @martin
AJ Hernandez


3

звонок cell.layoutIfNeeded()внутрь cellForRowAtработал у меня на ios 10 и ios 11, но не на ios 9.

Чтобы получить эту работу и на ios 9, я позвонил, cell.layoutSubviews()и это помогло.


2

Прикрепляем скриншот для вашей справкиДля меня ни один из этих подходов не работал, но я обнаружил, что метка явно Preferred Widthзадана в Interface Builder. Удаление этого (снятие флажка «Явный») и последующее использование UITableViewAutomaticDimensionсработало, как ожидалось.


2

Я попробовал все решения на этой странице, но снятие флажка с использованием классов размеров и повторная проверка решило мою проблему.

Изменить: снятие отметки с классов размеров вызывает множество проблем на раскадровке, поэтому я попробовал другое решение. Я заполнил свое табличное представление в контроллерах viewDidLoadи viewWillAppearметодах представления. Это решило мою проблему.


1

У меня проблема с изменением размера метки, поэтому мне нужно просто сделать
chatTextLabel.text = chatMessage.message chatTextLabel? .UpdateConstraints () после настройки текста

// полный код

func setContent() {
    chatTextLabel.text = chatMessage.message
    chatTextLabel?.updateConstraints()

    let labelTextWidth = (chatTextLabel?.intrinsicContentSize().width) ?? 0
    let labelTextHeight = chatTextLabel?.intrinsicContentSize().height

    guard labelTextWidth < originWidth && labelTextHeight <= singleLineRowheight else {
      trailingConstraint?.constant = trailingConstant
      return
    }
    trailingConstraint?.constant = trailingConstant + (originWidth - labelTextWidth)

  }

1

В моем случае я обновлялся в другом цикле. Таким образом, высота tableViewCell была обновлена ​​после установки labelText. Я удалил асинхронный блок.

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
     let cell = tableView.dequeueReusableCell(withIdentifier:Identifier, for: indexPath) 
     // Check your cycle if update cycle is same or not
     // DispatchQueue.main.async {
        cell.label.text = nil
     // }
}

У меня тоже есть асинхронный блок, но он нужен? Альтернативы нет?
Назар Медейрос,

1

Просто убедитесь, что вы не устанавливаете текст метки в методе делегата willdisplaycell представления таблицы. Задайте текст метки в методе делегата cellForRowAtindexPath для динамического расчета высоты.

Пожалуйста :)


1

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


1
Я пробовал все другие решения, только ваше решение сработало, спасибо, что поделились им
tamtoum1987,

1

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

- (void)viewDidAppear:(BOOL)animated
{
  [super viewDidAppear:animated];
  [self.tableView reloadData];
}

Это было намного лучше, чем помогло мне. Это не исправило 100% ячейки, но до 80% принимали их фактический размер. Большое спасибо
TheTravloper

1

Только для iOS 12+, начиная с 2019 г. ...

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

Похоже, что

        cell.layoutIfNeeded()
        return cell

исправлю это. (Вы, конечно, теряете производительность.)

Такова жизнь с Apple.


0

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

[self.tableView reloadData];

Я действительно пробовал

[cell layoutIfNeeded];

но это не сработало.


0

В Swift 3. Мне приходилось вызывать self.layoutIfNeeded () каждый раз, когда я обновляю текст повторно используемой ячейки.

import UIKit
import SnapKit

class CommentTableViewCell: UITableViewCell {

    static let reuseIdentifier = "CommentTableViewCell"

    var comment: Comment! {
        didSet {
            textLbl.attributedText = comment.attributedTextToDisplay()
            self.layoutIfNeeded() //This is a fix to make propper automatic dimentions (height).
        }
    }

    internal var textLbl = UILabel()

    override func layoutSubviews() {
        super.layoutSubviews()

        if textLbl.superview == nil {
            textLbl.numberOfLines = 0
            textLbl.lineBreakMode = .byWordWrapping
            self.contentView.addSubview(textLbl)
            textLbl.snp.makeConstraints({ (make) in
                make.left.equalTo(contentView.snp.left).inset(10)
                make.right.equalTo(contentView.snp.right).inset(10)
                make.top.equalTo(contentView.snp.top).inset(10)
                make.bottom.equalTo(contentView.snp.bottom).inset(10)
            })
        }
    }
}

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let comment = comments[indexPath.row]
        let cell = tableView.dequeueReusableCell(withIdentifier: CommentTableViewCell.reuseIdentifier, for: indexPath) as! CommentTableViewCell
        cell.selectionStyle = .none
        cell.comment = comment
        return cell
    }

commentsTableView.rowHeight = UITableViewAutomaticDimension
    commentsTableView.estimatedRowHeight = 140

0

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

Пришлось добавить следующее в viewDidLoad ().

DispatchQueue.main.async {

        self.tableView.reloadData()

        self.tableView.setNeedsLayout()
        self.tableView.layoutIfNeeded()

        self.tableView.reloadData()

    }

Вышеупомянутая комбинация reloadData, setNeedsLayout и layoutIfNeeded работала, но не другие. Однако может быть специфическим для ячеек в проекте. И да, пришлось дважды вызывать reloadData, чтобы он заработал.

Также установите следующее в viewDidLoad

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = MyEstimatedHeight

В tableView (_ tableView: UITableView, cellForRowAt indexPath: IndexPath)

cell.setNeedsLayout()
cell.layoutIfNeeded() 

Мой аналог reloadData/beginUpdates/endUpdates/reloadDataтоже работает; reloadData нужно позвонить во второй раз. Не нужно заворачивать async.
Ray

0

Я столкнулся с этой проблемой и исправил ее, переместив код инициализации представления / метки ОТ tableView(willDisplay cell:)ДО tableView(cellForRowAt:).


что НЕ рекомендуется для инициализации ячейки. willDisplayбудет иметь лучшую производительность, чем cellForRowAt. Используйте самую последнюю только для создания нужной ячейки.
Мартин

Спустя 2 года я читаю тот старый комментарий, который написал. Это был плохой совет. Даже если у willDisplayвас лучшая производительность, рекомендуется инициализировать пользовательский интерфейс ячейки, cellForRowAtкогда макет является автоматическим. Действительно, макет вычисляется UIKit после cellForRowAt и до willDisplay. Поэтому, если высота вашей ячейки зависит от ее содержимого, инициализируйте содержимое метки (или что-то еще) в cellForRowAt.
Мартин

-1
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{


//  call the method dynamiclabelHeightForText
}

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

-(int)dynamiclabelHeightForText:(NSString *)text :(int)width :(UIFont *)font
{

    CGSize maximumLabelSize = CGSizeMake(width,2500);

    CGSize expectedLabelSize = [text sizeWithFont:font
                                constrainedToSize:maximumLabelSize
                                    lineBreakMode:NSLineBreakByWordWrapping];


    return expectedLabelSize.height;


}

Этот код помогает вам найти динамическую высоту для отображения текста на этикетке.


На самом деле, Рамеш, в этом нет необходимости, если установлены свойства tableView.estimatedRowHeight и tableView.rowHeight = UITableViewAutomaticDimension. Кроме того, убедитесь, что пользовательская ячейка имеет соответствующие ограничения для различных виджетов и contentView.
BonanzaDriver
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.