Le altezze delle celle dinamiche di UITableView vengono corrette solo dopo un po 'di scorrimento


117

Ho UITableViewun personalizzato UITableViewCelldefinito in uno storyboard utilizzando il layout automatico. La cella ha diverse multilineeUILabels .

Le UITableViewsembra l'altezza delle celle correttamente calcolare, ma per le prime cellule che l'altezza non è adeguatamente suddiviso tra le etichette. Dopo aver fatto scorrere un po ', tutto funziona come previsto (anche le celle che inizialmente erano errate).

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

C'è qualcosa che potrei perdere, che sta facendo sì che il layout automatico non sia in grado di fare il suo lavoro le prime volte?

Ho creato un progetto di esempio che dimostra questo comportamento.

Progetto di esempio: parte superiore della vista tabella dal progetto di esempio al primo caricamento. Progetto di esempio: stesse celle dopo lo scorrimento verso il basso e il backup.


1
Sto anche affrontando questo problema, ma la risposta di seguito non funziona per me, Qualsiasi aiuto su questo
Shangari C

1
affrontando lo stesso problema aggiungendo layoutIfNeeded for cell non funziona anche per me? altri suggerimenti
Max

1
Ottimo posto. Questo è ancora un problema nel 2019: /
Fattie

Ho trovato ciò che mi ha aiutato nel seguente link: è una discussione piuttosto completa sulle celle di visualizzazione della tabella ad altezza variabile: stackoverflow.com/a/18746930/826946
Andy Weinstein

Risposte:


139

Non so che sia chiaramente documentato o meno, ma l'aggiunta di [cell layoutIfNeeded]celle prima di restituire risolve il problema.

- (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
Mi chiedo però, perché è necessario solo la prima volta? Funziona altrettanto bene se metto la chiamata a -layoutIfNeedednel cellulare -awakeFromNib. Preferisco chiamare solo layoutIfNeededdove so perché è necessario.
blackp

2
Ha funzionato per me! E mi piacerebbe sapere perché è necessario!
Mustafa

Non ha funzionato se si rende la classe di dimensioni, quella diversa da qualsiasi X qualsiasi
Andrei Konstantinov

@AndreyKonstantinov Sì, non funziona con la classe di taglia, come faccio a farlo funzionare con la classe di taglia?
z22

Funziona senza classe di taglia, qualsiasi soluzione con classe di taglia?
Yuvrajsinh

38

Questo ha funzionato per me quando altre soluzioni simili non funzionavano:

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

Questo sembra un vero bug poiché ho molta familiarità con AutoLayout e come usare UITableViewAutomaticDimension, tuttavia occasionalmente mi imbatto ancora in questo problema. Sono contento di aver finalmente trovato qualcosa che funziona come soluzione alternativa.


1
Migliore della risposta accettata in quanto viene chiamata solo quando la cella viene caricata da xib invece di ogni visualizzazione cella. Anche molte meno righe di codice.
Robert Wagstaff

3
Non dimenticare di chiamaresuper.didMoveToSuperview()
cicerocamargo

@cicerocamargo Apple dice di didMoveToSuperview: "L'implementazione predefinita di questo metodo non fa nulla."
Ivan Smetanin

4
@IvanSmetanin non importa se l'implementazione predefinita non fa nulla, dovresti comunque chiamare il super metodo in quanto potrebbe effettivamente fare qualcosa in futuro.
TNguyen

(Solo btw dovresti DEFINITAMENTE chiamare super. Su quello. Non ho mai visto un progetto in cui il team nel complesso non sottoclasse più di una volta le celle, quindi, ovviamente devi essere sicuro di prenderle tutte. E come questione separata solo quello che ha detto TNguyen.)
Fattie

22

Aggiungendo [cell layoutIfNeeded]a cellForRowAtIndexPathnon funziona per cellule che sono inizialmente lo scrolling out-of-view.

Né lo fa anteporre [cell setNeedsLayout].

Devi ancora scorrere alcune celle verso l'esterno e tornare alla visualizzazione per ridimensionarle correttamente.

Questo è piuttosto frustrante poiché la maggior parte degli sviluppatori ha Dynamic Type, AutoLayout e Self-Sizing Cells che funzionano correttamente, ad eccezione di questo caso fastidioso. Questo bug ha un impatto su tutti i miei controller di visualizzazione della tabella "più alti".


Stessa cosa per me. quando scorro per la prima volta, la dimensione della cella è 44px. se scorro per nascondere la cella e tornare indietro, è dimensionata correttamente. Hai trovato una soluzione?
Max

Grazie @ ashy_32bit questo ha risolto anche per me.
ImpurestClub

sembrerebbe essere un semplice bug, @Scenario giusto? roba orribile.
Fattie

3
L'uso al [cell layoutSubviews]posto di layoutIfNeeded potrebbe essere una possibile soluzione. Fare riferimento stackoverflow.com/a/33515872/1474113
ypresto

14

Ho avuto la stessa esperienza in uno dei miei progetti.

Perché succede?

Cella progettata in Storyboard con una certa larghezza per alcuni dispositivi. Ad esempio 400px. Ad esempio, la tua etichetta ha la stessa larghezza. Quando viene caricato dallo storyboard, ha una larghezza di 400 px.

Ecco un problema:

tableView:heightForRowAtIndexPath: chiamato prima del layout della cella è sottoviste.

Quindi ha calcolato l'altezza per l'etichetta e la cella con larghezza 400 px. Ma corri su un dispositivo con schermo, ad esempio 320px. E questa altezza calcolata automaticamente non è corretta. Solo perché le celle si layoutSubviewsverificano solo dopo tableView:heightForRowAtIndexPath: Anche se si imposta preferredMaxLayoutWidthmanualmente l'etichetta inlayoutSubviews esso non aiuta.

La mia soluzione:

1) Sottoclasse UITableViewe sovrascrittura dequeueReusableCellWithIdentifier:forIndexPath:. Imposta la larghezza della cella uguale alla larghezza della tabella e forza il layout della cella.

- (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) Sottoclasse UITableViewCell. Imposta preferredMaxLayoutWidthmanualmente le tue etichette in layoutSubviews. Inoltre è necessario il layout manuale contentView, perché non viene eseguito automaticamente dopo la modifica del frame della cella (non so perché, ma lo è)

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

1
Quando mi sono imbattuto in questo problema, dovevo anche assicurarmi che il frame della tableview fosse corretto, quindi ho chiamato [self.tableview layoutIfNeeded] prima che la tableview fosse popolata (in viewDidLoad).
GK100

Non ho mai smesso di pensare che avesse a che fare con una larghezza errata. cell.frame.size.width = tableview.frame.widthpoi cell.layoutIfNeeded()nella cellForRowAtfunzione ha fatto il trucco per me
Cam Connor

10

Ho un problema simile, al primo caricamento, l'altezza della riga non è stata calcolata ma dopo un po 'di scorrimento o sono passato a un'altra schermata e torno a questa schermata, le righe vengono calcolate. Al primo caricamento i miei articoli vengono caricati da Internet e al secondo carico i miei articoli vengono caricati prima da Core Data e ricaricati da Internet e ho notato che l'altezza delle righe viene calcolata al ricaricamento da Internet. Quindi ho notato che quando tableView.reloadData () viene chiamato durante l'animazione segue (stesso problema con push e present segue), l'altezza della riga non è stata calcolata. Quindi ho nascosto la tableview durante l'inizializzazione della vista e ho inserito un caricatore di attività per evitare un brutto effetto all'utente e chiamo tableView.reloadData dopo 300 ms e ora il problema è risolto. Penso che sia un bug di UIKit ma questa soluzione alternativa fa il trucco.

Ho inserito queste righe (Swift 3.0) nel mio gestore di completamento del caricamento degli articoli

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

Questo spiega perché per alcune persone, mettere un reloadData in layoutSubviews risolve il problema


Questa è l'unica WA che ha funzionato anche per me. Tranne che non uso isHidden, ma imposto l'alfa della tabella su 0 quando aggiungo l'origine dati, quindi su 1 dopo il ricaricamento.
PJ_Finnegan

6

nessuna delle soluzioni di cui sopra ha funzionato per me, ciò che ha funzionato è questa ricetta di una magia: chiamale in questo ordine:

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

i miei dati tableView sono popolati da un servizio web, nella richiamata della connessione scrivo le righe precedenti.


1
Per Swift 5 questo ha funzionato anche nella visualizzazione della raccolta, dio ti benedica, fratello.
Govani Dhruv Vijaykumar,

Per me, questo ha restituito l'altezza e il layout corretti della cella, ma non conteneva dati
Darrow Hartman,

Prova setNeedsDisplay () dopo queste righe
JAHelia

5

Nel mio caso l'ultima riga della UILabel è stata troncata quando la cella è stata visualizzata per la prima volta. È successo in modo abbastanza casuale e l'unico modo per ridimensionarlo correttamente era scorrere la cella fuori dalla vista e riportarla indietro. Ho provato tutte le possibili soluzioni visualizzate finora (layoutIfNeeded..reloadData) ma niente ha funzionato per me. Il trucco era impostare "Autoshrink" su Minimuum Font Scale (0,5 per me). Provaci


questo ha funzionato per me, senza applicare nessuna delle altre soluzioni elencate sopra. Ottima scoperta.
mckeejm

2
Ma questo restringe automaticamente il testo ... perché si dovrebbe voler avere diverse dimensioni di testo in una vista tabella? Sembra terribile.
lucius degeer,

5

Aggiungere un vincolo per tutto il contenuto all'interno di una cella personalizzata della vista tabella, quindi stimare l'altezza della riga della vista tabella e impostare l'altezza della riga sulla dimensione automatica con un caricamento della vista:

    override func viewDidLoad() {
    super.viewDidLoad()

    tableView.estimatedRowHeight = 70
    tableView.rowHeight = UITableViewAutomaticDimension
}

Per risolvere il problema di caricamento iniziale, applicare il metodo layoutIfNeeded con in una cella di visualizzazione tabella personalizzata:

class CustomTableViewCell: UITableViewCell {

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

È importante impostare estimatedRowHeightun valore> 0 e non UITableViewAutomaticDimension(che è -1), altrimenti l'altezza della riga automatica non funzionerà.
PJ_Finnegan

5

Ho provato la maggior parte delle risposte a questa domanda e non sono riuscito a far funzionare nessuna di esse. L'unica soluzione funzionale che ho trovato è stata quella di aggiungere quanto segue alla mia UITableViewControllersottoclasse:

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

La UIView.performWithoutAnimationchiamata è richiesta, altrimenti vedrai la normale animazione della vista tabella durante il caricamento del controller della vista.


funzionante e decisamente meglio che invocare due volte reloadData
Jimmy George Thomas

2
Magia nera. Farlo dentro viewWillAppearnon ha funzionato per me, ma farlo dentro lo ha viewDidAppearfatto.
Martin,

Questa soluzione "ha funzionato" per me quando è stata implementata in viewDidAppear. Non è affatto ottimale per l'esperienza dell'utente poiché il contenuto della tabella salta quando si verifica il dimensionamento corretto.
richardpiazza

incredibile ho provato un sacco di esempi ma fallisce tu sei un demone
Shakeel Ahmed

Come @martin
AJ Hernandez il


3

chiamando cell.layoutIfNeeded()dentrocellForRowAt funzionato per me su ios 10 e ios 11, ma non su ios 9.

per ottenere questo lavoro anche su iOS 9, chiamo cell.layoutSubviews()e ha funzionato.


2

Attacco screenshot per il tuo riferimentoPer me nessuno di questi approcci ha funzionato, ma ho scoperto che l'etichetta aveva un Preferred Widthset esplicito in Interface Builder. Rimuoverlo (deselezionando "Explicit") e quindi utilizzare ha UITableViewAutomaticDimensionfunzionato come previsto.


2

Ho provato tutte le soluzioni in questa pagina, ma deselezionare le classi di dimensioni d'uso e ricontrollarlo ha risolto il problema.

Modifica: deselezionare le classi di dimensione causa molti problemi sullo storyboard, quindi ho provato un'altra soluzione. Ho popolato la mia visualizzazione tabella nei metodi viewDidLoade nel controller della vista viewWillAppear. Questo ha risolto il mio problema.


1

Ho il problema con il ridimensionamento dell'etichetta, quindi devo solo fare
chatTextLabel.text = chatMessage.message chatTextLabel? .UpdateConstraints () dopo aver impostato il testo

// codice completo

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

Nel mio caso, mi stavo aggiornando in un altro ciclo. Quindi l'altezza di tableViewCell è stata aggiornata dopo l'impostazione di labelText. Ho eliminato il blocco asincrono.

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

Ho anche un blocco asincrono ma è necessario? Non ci sono alternative?
Nazar Medeiros,

1

Assicurati solo di non impostare il testo dell'etichetta nel metodo delegato "willdisplaycell" della visualizzazione tabella. Imposta il testo dell'etichetta nel metodo delegato "cellForRowAtindexPath" per il calcolo dinamico dell'altezza.

Prego :)


1

Nel mio caso, il problema era causato da una visualizzazione dello stack nella cella. Apparentemente è un bug. Una volta rimosso, il problema è stato risolto.


1
Ho provato tutte le altre soluzioni, solo la tua soluzione ha funzionato per ringraziarti di averla condivisa
tamtoum1987

1

Il problema è che le celle iniziali vengono caricate prima di avere un'altezza di riga valida. La soluzione è forzare il ricaricamento di una tabella quando viene visualizzata la visualizzazione.

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

questa è stata la soluzione migliore che mi ha aiutato. Non ha corretto il 100% della cella, ma fino all'80% ha preso la dimensione effettiva. Grazie mille
TheTravloper

1

Solo per iOS 12+, dal 2019 in poi ...

Un esempio continuo della bizzarra incompetenza occasionale di Apple, in cui i problemi continuano letteralmente per anni.

Sembra che sia così

        cell.layoutIfNeeded()
        return cell

lo risolverà. (Stai perdendo alcune prestazioni ovviamente.)

Questa è la vita con Apple.


0

Nel mio caso, il problema con l'altezza della cella si verifica dopo il caricamento della vista tabella iniziale e si verifica un'azione dell'utente (toccando un pulsante in una cella che ha l'effetto di modificare l'altezza della cella). Non sono riuscito a far sì che la cella cambi la sua altezza a meno che non lo faccia:

[self.tableView reloadData];

Ci ho provato

[cell layoutIfNeeded];

ma non ha funzionato.


0

In Swift 3. ho dovuto chiamare self.layoutIfNeeded () ogni volta che aggiorno il testo della cella riutilizzabile.

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

Nessuna delle soluzioni di cui sopra ha funzionato, ma la seguente combinazione dei suggerimenti ha funzionato.

Ho dovuto aggiungere quanto segue in viewDidLoad ().

DispatchQueue.main.async {

        self.tableView.reloadData()

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

        self.tableView.reloadData()

    }

La combinazione di cui sopra di reloadData, setNeedsLayout e layoutIfNeeded ha funzionato ma non qualsiasi altra. Potrebbe essere specifico per le celle del progetto. E sì, ho dovuto invocare reloadData due volte per farlo funzionare.

Impostare anche quanto segue in viewDidLoad

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = MyEstimatedHeight

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

cell.setNeedsLayout()
cell.layoutIfNeeded() 

Anche il mio simile reloadData/beginUpdates/endUpdates/reloadDatafunziona; reloadData deve essere chiamato una seconda volta. Non è necessario avvolgerlo async.
raggio

0

Mi sono imbattuto in questo problema e risolto spostando mio avviso / etichetta di codice di inizializzazione DA tableView(willDisplay cell:)AL tableView(cellForRowAt:).


che NON è il modo consigliato per inizializzare una cella. willDisplayavrà prestazioni migliori rispetto a cellForRowAt. Usa l'ultimo solo per istanziare la cella giusta.
Martin,

2 anni dopo, sto leggendo quel vecchio commento che ho scritto. Questo è stato un cattivo consiglio. Anche se willDisplayhanno prestazioni migliori, si consiglia di inizializzare l'interfaccia utente del cellulare cellForRowAtquando il layout è automatico. In effetti, il layout viene calcolato da UIKit dopo cellForRowAt e prima di willDisplay. Quindi, se l'altezza della cella dipende dal suo contenuto, inizializza il contenuto dell'etichetta (o qualsiasi altra cosa) in cellForRowAt.
Martin

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


//  call the method dynamiclabelHeightForText
}

utilizzare il metodo precedente che restituisce dinamicamente l'altezza della riga. E assegna la stessa altezza dinamica all'etichetta che stai utilizzando.

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


}

Questo codice ti aiuta a trovare l'altezza dinamica per la visualizzazione del testo nell'etichetta.


In realtà Ramesh, questo non dovrebbe essere necessario fintanto che le proprietà tableView.estimatedRowHeight e tableView.rowHeight = UITableViewAutomaticDimension sono impostate. Inoltre, assicurati che la cella personalizzata abbia vincoli appropriati sui vari widget e su contentView.
BonanzaDriver
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.