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там, де я знаю, чому це потрібно.
чорний

2
Працювали для мене! І я хотів би знати, чому це потрібно!
Мустафа

Не вийшло, якщо ви візуалізуєте клас розмірів, який відрізняється від будь-якого X будь-якого
Андрій Константинов

@AndreyKonstantinov. Так, це не працює з розмірним класом, як мені змусити його працювати з класом розміру?
z22

Це працює без розмірного класу, будь-яке рішення з класом розміру?
Yuvrajsinh

38

Це працювало для мене, коли інших подібних рішень не було:

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

Це здається фактичною помилкою, оскільки я добре знайомий з AutoLayout та способом використання UITableViewAutomaticDimension, однак я все ще час від часу стикаюся з цією проблемою. Я радий, що нарешті знайшов щось, що працює як вирішення.


1
Краще, ніж прийнята відповідь, оскільки вона називається лише тоді, коли комірка завантажується з xib замість кожного відображення комірки. Набагато менше рядків коду теж.
Роберт Вагстафф

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

@cicerocamargo Apple говорить про didMoveToSuperview: "Реалізація цього методу за замовчуванням нічого не робить".
Іван Сметанін

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

(Тільки btw ви повинні ВИКОНАНО зателефонувати супер. На цьому. Я ніколи не бачив проекту, де команда загалом не підкласирує більше ніж один раз комірки, так що, звичайно, ви повинні бути впевнені, щоб забрати їх усі. І як окреме питання тільки що сказав TNguyen).
Fattie

22

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

Не застосовується також до цього [cell setNeedsLayout].

Ви все ще повинні прокрутити певні комірки назад та переглянути їх, щоб правильно змінити розмір.

Це досить засмучує, оскільки більшість розробників мають динамічний тип, автоматичне розташування та саморозмірні клітини, які працюють належним чином - за винятком цього прикрого випадку. Ця помилка впливає на всі мої контролери "вище" таблиці.


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

Дякую @ ashy_32bit це вирішило це і для мене.
ImpurestClub

здавалося б, це звичайна помилка, @Scenario так? жахливі речі.
Fattie

3
Використання [cell layoutSubviews]замість layoutIfNeeded можливо виправити. Див stackoverflow.com/a/33515872/1474113
ypresto

14

Я мав такий же досвід в одному зі своїх проектів.

Чому це відбувається?

Осередок, розроблений на аркуші розгортки, має деяку ширину для певного пристрою. Наприклад 400px. Наприклад, ваша мітка має однакову ширину. Коли він завантажується з раскадровки, він має ширину 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

У мене є аналогічна проблема, при першому завантаженні висота рядка не була обчислена, але після деякої прокрутки або переходу на інший екран, і я повертаюся до цього екрану, обчислюються рядки. Під час першого завантаження мої елементи завантажуються з Інтернету, а при другому завантажуються мої елементи спочатку з основних даних і перезавантажуються з Інтернету. Тож я помітив, що при виклику tableView.reloadData () під час анімації segue (та ж проблема з натисканням та наявним segue) висота рядка не обчислювалася. Тому я приховав перегляд таблиці при ініціалізації подання і поставив завантажувач активності, щоб запобігти негарному ефекту для користувача, і я викликаю 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
Для швидкої 5 це спрацювало навіть у вигляді колекції, Бог благословив тебе, брате.
Говані Друв Віякумар

Для мене це повернуло правильну висоту і розташування комірки, але в ній не було даних
Дарроу Хартман

Спробуйте setNeedsDisplay () після цих рядків
JAHelia

5

У моєму випадку останній рядок UILabel був усічений, коли комірка була виведена вперше. Це сталося досить випадковим чином, і єдиний спосіб правильно розмістити його - прокрутити клітинку з виду та повернути її назад. Я спробував усі можливі рішення, що відображалися до цих пір (layoutIfNeeded..reloadData), але для мене нічого не вийшло. Хитрість полягала в тому, щоб встановити "Автосшинку" на шкалі шрифту Minimuum (0,5 для мене). Спробувати


це працювало для мене, не застосовуючи жодного з інших перелічених вище рішень. Чудова знахідка.
mckeejm

2
Але це автоматично заповнює текст ... чому б ви хотіли, щоб текст різних розмірів був у вигляді таблиці? Виглядає жахливо.
Луцій Дегеер

5

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

    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 Ернандес


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.
промінь

0

Я зіткнувся з цією проблемою і виправив її, перемістивши свій код перегляду / мітки ініціалізації ВІД tableView(willDisplay cell:)ДО tableView(cellForRowAt:).


що НЕ є рекомендованим способом ініціалізації комірки. willDisplayматиме кращі показники, ніж cellForRowAt. Використовуйте останні терміни лише для інстанціювання потрібної клітини.
Мартін

Через 2 роки я читаю той старий коментар, який я записав. Це була погана порада. Навіть якщо ви willDisplayмаєте кращі показники, iS рекомендує ініціалізувати інтерфейс вашої комірки, cellForRowAtколи компонування буде автоматичним. Дійсно, макет обчислюється UIKit після cellForRowAt et перед 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.