Dynamiczne wysokości komórek UITableView poprawiają się tylko po pewnym przewinięciu


117

Mam UITableViewniestandardowy UITableViewCellzdefiniowany w scenorysie przy użyciu układu automatycznego. Komórka ma kilka multilinii UILabels.

Że UITableViewwydaje się właściwie oblicz wysokościach komórkowych, ale dla kilku pierwszych komórek wysokość nie jest właściwie podzielone pomiędzy etykietami. Po pewnym przewinięciu wszystko działa zgodnie z oczekiwaniami (nawet komórki, które początkowo były niepoprawne).

- (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;
}

Czy jest coś, czego może mi brakować, co powoduje, że auto layout nie jest w stanie wykonać swojej pracy kilka pierwszych razy?

Stworzyłem przykładowy projekt, który demonstruje takie zachowanie.

Przykładowy projekt: Widok góry tabeli z przykładowego projektu przy pierwszym załadowaniu. Przykładowy projekt: te same komórki po przewinięciu w dół i kopii zapasowej.


1
Ja też mam do czynienia z tym problemem, ale poniższa odpowiedź nie działa dla mnie, Jakakolwiek pomoc w tej sprawie
Shangari C

1
napotykając ten sam problem, dodawanie layoutuIfNeeded for cell nie działa dobrze? więcej sugestii
maksymalnie

1
Świetne miejsce. To wciąż jest problem w 2019 roku: /
Fattie

To, co mi pomogło, znalazłem w poniższym linku - jest to dość obszerne omówienie komórek widoku tabeli o zmiennej wysokości: stackoverflow.com/a/18746930/826946
Andy Weinstein

Odpowiedzi:


139

Nie wiem, czy jest to wyraźnie udokumentowane, czy nie, ale dodanie [cell layoutIfNeeded]przed zwróceniem komórki rozwiązuje problem.

- (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
Zastanawiam się jednak, dlaczego jest to konieczne tylko za pierwszym razem? Działa to równie dobrze, jeśli wykonam połączenie -layoutIfNeededw komórce -awakeFromNib. Wolę dzwonić tylko layoutIfNeededtam, gdzie wiem, dlaczego jest to konieczne.
blackp

2
Pracował dla mnie! Chciałbym wiedzieć, dlaczego jest to wymagane!
Mustafa

Nie zadziałało, jeśli wyrenderujesz klasę wielkości, która różni się od dowolnego X
Andrei Konstantinov

@AndreyKonstantinov Tak, to nie działa z klasą wielkości, jak mam to zrobić z klasą wielkości?
z22

Działa bez klasy wielkości, jakieś rozwiązanie z klasą wielkości?
Yuvrajsinh

38

U mnie to działało, gdy inne podobne rozwiązania nie:

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

Wydaje się, że jest to rzeczywisty błąd, ponieważ znam bardzo dobrze układ AutoLayout i sposób korzystania z UITableViewAutomaticDimension, jednak nadal czasami napotykam ten problem. Cieszę się, że w końcu znalazłem coś, co działa jako obejście.


1
Lepsza niż akceptowana odpowiedź, ponieważ jest wywoływana tylko wtedy, gdy komórka jest ładowana z xib, a nie z każdego wyświetlenia komórki. Znacznie mniej linii kodu.
Robert Wagstaff

3
Nie zapomnij zadzwonićsuper.didMoveToSuperview()
cicerocamargo

@cicerocamargo Apple mówi o didMoveToSuperview: „Domyślna implementacja tej metody nic nie robi”.
Ivan Smetanin

4
@IvanSmetanin nie ma znaczenia, czy domyślna implementacja nic nie robi, nadal powinieneś wywoływać super metodę, ponieważ może ona faktycznie zrobić coś w przyszłości.
TNguyen

(Tak przy okazji, powinieneś ZDECYDOWANIE zadzwonić do super. W tym przypadku. Nigdy nie widziałem projektu, w którym zespół ogólnie nie tworzy podklas więcej niż raz komórek, więc oczywiście musisz mieć pewność, że wybrałeś je wszystkie. oddzielna kwestia, właśnie to, co powiedział TNguyen.)
Fattie

22

Dodanie [cell layoutIfNeeded]w cellForRowAtIndexPathnie działa dla komórek, które są początkowo przewinęły out-of-view.

Ani też poprzedzanie go [cell setNeedsLayout].

Nadal musisz przewinąć niektóre komórki i wrócić do widoku, aby poprawnie zmienić rozmiar.

Jest to dość frustrujące, ponieważ większość programistów ma poprawnie działające dynamiczne typy, układy AutoLayout i Self-Sizing Cells - z wyjątkiem tego irytującego przypadku. Ten błąd dotyczy wszystkich moich „wyższych” kontrolerów widoku tabeli.


Dla mnie to samo. kiedy przewijam pierwszy raz, komórka ma rozmiar 44px. jeśli przewinę, aby usunąć komórkę z pola widzenia i wrócę, rozmiar jej jest właściwy. Czy znalazłeś jakieś rozwiązanie?
maksymalnie

Dziękuję @ ashy_32bit, to również rozwiązało to dla mnie.
ImpurestClub

wydaje się, że to zwykły błąd, @Scenario, prawda? okropne rzeczy.
Fattie

3
Użycie [cell layoutSubviews]zamiast layoutIfNeeded może być możliwe do naprawienia. Skorzystaj ze strony stackoverflow.com/a/33515872/1474113
ypresto

14

Miałem takie samo doświadczenie w jednym z moich projektów.

Dlaczego tak się dzieje?

Komórka zaprojektowana w Storyboard z pewną szerokością dla niektórych urządzeń. Na przykład 400px. Na przykład Twoja etykieta ma taką samą szerokość. Kiedy ładuje się z storyboardu, ma szerokość 400px.

Oto problem:

tableView:heightForRowAtIndexPath: wywołane przed układem komórki to podwidoki.

Więc obliczył wysokość etykiety i komórki o szerokości 400 pikseli. Ale uruchamiasz na urządzeniu z ekranem, na przykład 320px. Ta automatycznie obliczona wysokość jest nieprawidłowa. Tylko dlatego, że komórka layoutSubviewsdzieje się dopiero po tableView:heightForRowAtIndexPath: Nawet jeśli preferredMaxLayoutWidthręcznie ustawisz etykietę, layoutSubviewsto nie pomaga.

Moje rozwiązanie:

1) Podklasa UITableViewi nadpisanie dequeueReusableCellWithIdentifier:forIndexPath:. Ustaw szerokość komórki równą szerokości tabeli i wymuś układ komórki.

- (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) Podklasa UITableViewCell. Ustaw preferredMaxLayoutWidthręcznie dla swoich etykiet w formacie layoutSubviews. Potrzebujesz również układu ręcznego contentView, ponieważ nie układa się automatycznie po zmianie ramki komórki (nie wiem dlaczego, ale tak jest)

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

1
Kiedy natknąłem się na ten problem, musiałem również upewnić się, że ramka widoku tabeli jest poprawna, więc wywołałem [self.tableview layoutIfNeeded] przed zapełnieniem widoku tabeli (w viewDidLoad).
GK100

Nigdy nie przestałem myśleć, że ma to związek z nieprawidłową szerokością. cell.frame.size.width = tableview.frame.widthNastępnie cell.layoutIfNeeded()w cellForRowAtfunkcji wystarczyły dla mnie
Cam Connor

10

Mam podobny problem, przy pierwszym ładowaniu wysokość wiersza nie została obliczona ale po jakimś przewinięciu lub przejściu do innego ekranu i wracam do tego ekranu są obliczane wiersze. Przy pierwszym ładowaniu moje elementy są ładowane z Internetu, a przy drugim ładowaniu moje elementy są najpierw ładowane z danych podstawowych i ponownie ładowane z Internetu. Zauważyłem, że wysokość wierszy jest obliczana przy przeładowaniu z Internetu. Zauważyłem więc, że gdy tableView.reloadData () jest wywoływana podczas animacji płynnej (ten sam problem z wypychaniem i obecnym płynem), wysokość wiersza nie została obliczona. Więc ukryłem widok tabeli podczas inicjalizacji widoku i umieściłem program ładujący aktywność, aby zapobiec brzydkiemu efektowi dla użytkownika, i wywołuję tableView.reloadData po 300 ms, a teraz problem został rozwiązany. Myślę, że to błąd UIKit, ale to obejście załatwia sprawę.

Umieściłem te wiersze (Swift 3.0) w moim module obsługi zakończenia ładowania przedmiotu

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

To wyjaśnia, dlaczego dla niektórych osób umieszczenie reloadData w layoutSubviews rozwiązuje problem


To jedyne WA, które też mi pracowało. Z wyjątkiem tego, że nie używam isHidden, ale ustawiam wartość alfa tabeli na 0 podczas dodawania źródła danych, a następnie na 1 po ponownym załadowaniu.
PJ_Finnegan

6

żadne z powyższych rozwiązań u mnie nie zadziałało, zadziałał ten przepis na magię: zadzwoń do nich w tej kolejności:

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

moje dane tableView są wypełniane z usługi internetowej, w wywołaniu zwrotnym połączenia piszę powyższe wiersze.


1
Dla Swift 5 działało to nawet w widoku kolekcji, niech Bóg cię błogosławi, brachu.
Govani Dhruv Vijaykumar

Dla mnie to zwróciło poprawną wysokość i układ komórki, ale nie zawierało żadnych danych
Darrow Hartman

Spróbuj setNeedsDisplay () po tych liniach
JAHelia

5

W moim przypadku ostatnia linia UILabel została obcięta, gdy komórka była wyświetlana po raz pierwszy. Zdarzyło się to dość przypadkowo i jedynym sposobem na poprawne określenie rozmiaru było przewinięcie komórki z widoku i przywrócenie jej. Wypróbowałem wszystkie możliwe rozwiązania wyświetlane do tej pory (layoutIfNeeded..reloadData), ale nic nie działało. Sztuczka polegała na ustawieniu „Autoshrink” na Minimuum Font Scale (dla mnie 0.5). Spróbuj


to zadziałało dla mnie, bez zastosowania innych rozwiązań wymienionych powyżej. Świetne znalezisko.
mckeejm

2
Ale to automatycznie zmniejsza tekst… dlaczego ktoś miałby chcieć mieć różne rozmiary tekstu w widoku tabeli? Wygląda okropnie.
lucius degeer

5

Dodaj ograniczenie dla całej zawartości w niestandardowej komórce widoku tabeli, a następnie oszacuj wysokość wiersza widoku tabeli i ustaw wysokość wiersza na automatyczny wymiar z wczytywaniu viewdid:

    override func viewDidLoad() {
    super.viewDidLoad()

    tableView.estimatedRowHeight = 70
    tableView.rowHeight = UITableViewAutomaticDimension
}

Aby rozwiązać ten problem z początkowym ładowaniem, zastosuj metodę layoutIfNeeded w niestandardowej komórce widoku tabeli:

class CustomTableViewCell: UITableViewCell {

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

Ważne jest, aby ustawić estimatedRowHeightwartość> 0, a nie UITableViewAutomaticDimension(czyli -1), w przeciwnym razie automatyczna wysokość wiersza nie będzie działać.
PJ_Finnegan

5

Wypróbowałem większość odpowiedzi na to pytanie i żadna z nich nie zadziałała. Jedynym funkcjonalnym rozwiązaniem, jakie znalazłem, było dodanie następujących elementów do mojej UITableViewControllerpodklasy:

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

UIView.performWithoutAnimationPołączenie jest konieczne, w przeciwnym razie pojawi się normalny animacji widoku tabeli jako ładunku widok kontrolera.


działa i zdecydowanie lepiej niż dwukrotne wywoływanie reloadData
Jimmy George Thomas

2
Czarna magia. Robienie tego viewWillAppearnie działało na mnie, ale robienie tego w viewDidAppearnie.
Martin,

To rozwiązanie „zadziałało” u mnie po zaimplementowaniu w pliku viewDidAppear. W żadnym wypadku nie jest to optymalne dla wygody użytkownika, ponieważ zawartość tabeli przeskakuje, gdy następuje właściwy rozmiar.
richardpiazza

niesamowite, próbowałem wielu przykładów, ale nie udaje ci się, jesteś demonem
Shakeel Ahmed,

To samo co @martin
AJ Hernandez


3

dzwonienie do cell.layoutIfNeeded()środka cellForRowAtdziałało dla mnie na iOS 10 i iOS 11, ale nie na iOS 9.

aby dostać tę pracę również na ios 9, dzwonię cell.layoutSubviews()i załatwiło sprawę.


2

Załączam zrzut ekranu dla twojego odniesieniaDla mnie żadne z tych podejść nie zadziałało, ale odkryłem, że etykieta ma wyraźny Preferred Widthzestaw w Interface Builder. Usunięcie tego (odznaczenie „Explicit”), a następnie użycie UITableViewAutomaticDimensiondziałało zgodnie z oczekiwaniami.


2

Wypróbowałem wszystkie rozwiązania na tej stronie, ale odznaczenie klas wielkości i ponowne sprawdzenie rozwiązało mój problem.

Edycja: odznaczenie klas wielkości powoduje wiele problemów w scenorysie, więc wypróbowałem inne rozwiązanie. Zapełniłem widok tabeli w kontrolerze widoku viewDidLoadi viewWillAppearmetodach. To rozwiązało mój problem.


1

Mam problem ze
zmianą rozmiaru etykiety, więc po skonfigurowaniu tekstu muszę po prostu zrobić chatTextLabel.text = chatMessage.message chatTextLabel? .UpdateConstraints ()

// pełny kod

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

W moim przypadku aktualizowałem w innym cyklu. Więc tableViewCell wysokość została zaktualizowana po ustawieniu labelText. Usunąłem blok asynchroniczny.

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
     // }
}

Mam też blok asynchroniczny, ale jest potrzebny? Czy nie ma alternatywy?
Nazar Medeiros

1

Tylko upewnij się, że nie ustawiasz tekstu etykiety w metodzie delegata „willdisplaycell” widoku tabeli. Ustaw tekst etykiety w metodzie delegata „cellForRowAtindexPath” na potrzeby dynamicznego obliczania wysokości.

Nie ma za co :)


1

W moim przypadku problem powodował widok stosu w komórce. Najwyraźniej to błąd. Po usunięciu problem został rozwiązany.


1
Wypróbowałem wszystkie inne rozwiązania, tylko twoje rozwiązanie działało, dziękuję za udostępnienie
tamtoum1987

1

Problem polega na tym, że początkowe komórki ładują się, zanim uzyskamy prawidłową wysokość wiersza. Sposób obejścia problemu polega na wymuszeniu ponownego załadowania tabeli, gdy pojawi się widok.

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

było to o wiele o wiele lepsze rozwiązanie, które mi pomogło. Nie poprawiło 100% komórki, ale do 80% przyjmowało ich rzeczywisty rozmiar.
Wielkie

1

Tylko dla iOS 12+, od 2019 r ...

Ciągły przykład sporadycznej, dziwacznej niekompetencji Apple, w której problemy trwają dosłownie latami.

Wydaje się, że tak jest

        cell.layoutIfNeeded()
        return cell

naprawi to. (Oczywiście tracisz trochę wydajności).

Takie jest życie z Apple.


0

W moim przypadku problem z wysokością komórki ma miejsce po załadowaniu początkowego widoku tabeli i wykonaniu akcji użytkownika (dotknięcie przycisku w komórce, która ma wpływ na zmianę wysokości komórki). Nie udało mi się zmusić komórki do zmiany jej wysokości, chyba że:

[self.tableView reloadData];

Próbowałem

[cell layoutIfNeeded];

ale to nie zadziałało.


0

W Swift 3. musiałem wywołać funkcję self.layoutIfNeeded () za każdym razem, gdy aktualizowałem tekst komórki wielokrotnego użytku.

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

Żadne z powyższych rozwiązań nie zadziałało, ale następująca kombinacja sugestii nie zadziałała.

Musiał dodać następujące elementy w viewDidLoad ().

DispatchQueue.main.async {

        self.tableView.reloadData()

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

        self.tableView.reloadData()

    }

Powyższa kombinacja reloadData, setNeedsLayout i layoutIfNeeded zadziałała, ale żadna inna. Może jednak być specyficzny dla komórek w projekcie. I tak, musiałem dwukrotnie wywołać reloadData, aby działało.

Ustaw również następujące elementy w viewDidLoad

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = MyEstimatedHeight

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

cell.setNeedsLayout()
cell.layoutIfNeeded() 

Mój podobny reloadData/beginUpdates/endUpdates/reloadDatateż działa; reloadData musi zostać wezwany po raz drugi. Nie musisz go zawijać async.
ray

0

Natknąłem się na ten problem i naprawiłem go, przenosząc kod inicjalizacji widoku / etykiety Z tableView(willDisplay cell:)DO tableView(cellForRowAt:).


co NIE jest zalecanym sposobem inicjalizacji komórki. willDisplaybędzie miał lepszą wydajność niż cellForRowAt. Użyj ostatniej tylko do zainicjowania właściwej komórki.
Martin

Dwa lata później czytam ten stary komentarz, który zapisałem. To była zła rada. Nawet jeśli willDisplaymasz lepszą wydajność, zaleca się zainicjowanie interfejsu użytkownika komórki, cellForRowAtgdy układ jest automatyczny. Rzeczywiście, układ jest obliczany przez UIKit po cellForRowAt et przed willDisplay. Jeśli więc wysokość komórki zależy od jej zawartości, zainicjuj zawartość etykiety (lub cokolwiek) w cellForRowAt.
Martin

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


//  call the method dynamiclabelHeightForText
}

użyj powyższej metody, która dynamicznie zwraca wysokość wiersza. I przypisz tę samą dynamiczną wysokość do używanej etykiety.

-(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;


}

Ten kod pomaga znaleźć dynamiczną wysokość tekstu wyświetlanego na etykiecie.


Właściwie Ramesh, nie powinno to być potrzebne, jeśli ustawione są właściwości tableView.estimatedRowHeight i tableView.rowHeight = UITableViewAutomaticDimension. Upewnij się również, że komórka niestandardowa ma odpowiednie ograniczenia dotyczące różnych widżetów i elementu contentView.
BonanzaDriver
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.