reloadData () di UITableView con altezze di celle dinamiche provoca uno scorrimento rapido


142

Penso che questo potrebbe essere un problema comune e mi chiedevo se ci fosse una soluzione comune ad esso.

Fondamentalmente, il mio UITableView ha altezze di cella dinamiche per ogni cella. Se non sono in cima a UITableView e io tableView.reloadData(), lo scorrimento verso l'alto diventa instabile.

Credo che ciò sia dovuto al fatto che, poiché ho ricaricato i dati, mentre sto scorrendo verso l'alto, UITableView sta ricalcolando l'altezza per ogni cella che viene visualizzata. Come posso mitigarlo o come ricaricare i dati da un determinato IndexPath alla fine di UITableView?

Inoltre, quando riesco a scorrere fino in cima, posso scorrere indietro e poi su, senza problemi senza saltare. Ciò è molto probabilmente perché le altezze di UITableViewCell erano già state calcolate.


Un paio di cose ... (1) Sì, puoi sicuramente ricaricare determinate file usando reloadRowsAtIndexPaths. Ma (2) cosa intendi con "jumpy" e (3) hai impostato un'altezza di riga stimata? (Sto solo cercando di capire se esiste una soluzione migliore che ti consenta di aggiornare la tabella in modo dinamico.)
Lyndsey Scott

@LyndseyScott, sì, ho impostato un'altezza di riga stimata. Per jumpy intendo che mentre scorro verso l'alto, le file si spostano verso l'alto. Credo che ciò sia dovuto al fatto che ho impostato un'altezza di riga stimata di 128 e, mentre scorro verso l'alto, tutti i miei messaggi sopra in UITableView sono più piccoli, quindi riduce l'altezza, facendo saltare il mio tavolo. Sto pensando di fare reloadRowsAtIndexPaths dalla riga xall'ultima riga nel mio TableView ... ma poiché sto inserendo nuove righe, non funzionerà, non posso sapere quale sarà la fine della mia vista del tavolo prima di ricaricare i dati.
David

2
@LyndseyScott non riesco ancora a risolvere il problema, c'è qualche buona soluzione?
Rad

1
Hai mai trovato una soluzione per questo problema? Sto riscontrando lo stesso identico problema riscontrato nel tuo video.
user3344977,

1
Nessuna delle risposte di seguito ha funzionato per me.
Srujan Simha,

Risposte:


221

Per evitare il salto dovresti risparmiare altezze di celle quando si caricano e dare il valore esatto in tableView:estimatedHeightForRowAtIndexPath:

Swift:

var cellHeights = [IndexPath: CGFloat]()

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    cellHeights[indexPath] = cell.frame.size.height
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return cellHeights[indexPath] ?? UITableView.automaticDimension
}

Obiettivo C:

// declare cellHeightsDictionary
NSMutableDictionary *cellHeightsDictionary = @{}.mutableCopy;

// declare table dynamic row height and create correct constraints in cells
tableView.rowHeight = UITableViewAutomaticDimension;

// save height
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    [cellHeightsDictionary setObject:@(cell.frame.size.height) forKey:indexPath];
}

// give exact height value
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSNumber *height = [cellHeightsDictionary objectForKey:indexPath];
    if (height) return height.doubleValue;
    return UITableViewAutomaticDimension;
}

1
Grazie, mi hai davvero salvato la giornata :) Funziona anche in objc
Artem Z.

3
Non dimenticare di inizializzare cellHeightsDictionary: cellHeightsDictionary = [NSMutableDictionary dictionary];
Gerharbo,

1
estimatedHeightForRowAtIndexPath:restituisce un doppio valore può causare un *** Assertion failure in -[UISectionRowData refreshWithSection:tableView:tableViewRowData:]errore. Per ripararlo, return floorf(height.floatValue);invece.
liushuaikobe,

Ciao @lgor, sto riscontrando lo stesso problema e sto provando a implementare la tua soluzione. Il problema che sto riscontrando è stimatoHeightForRowAtIndexPath viene chiamato prima di willDisplayCell, quindi l'altezza della cella non viene calcolata quando viene stimataHeightForRowAtIndexPath. Qualsiasi aiuto?
Madhuri,

1
Le altezze effettive di @Madhuri dovrebbero essere calcolate in "heightForRowAtIndexPath", che viene chiamato per ogni cella sullo schermo appena prima di willDisplayCell, che imposterà l'altezza nel dizionario per un uso successivo in stimatoRowHeight (al ricaricamento della tabella).
Donnit,

109

Swift 3 versione della risposta accettata.

var cellHeights: [IndexPath : CGFloat] = [:]


func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    cellHeights[indexPath] = cell.frame.size.height
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return cellHeights[indexPath] ?? 70.0 
}

Grazie ha funzionato benissimo! infatti sono stato in grado di rimuovere la mia implementazione di func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {, questo gestisce tutto il calcolo dell'altezza di cui ho bisogno.
Natalia

Dopo aver lottato molte ore con il salto persistente, ho capito che avevo dimenticato di aggiungere UITableViewDelegatealla mia classe. La conformità a tale protocollo è necessaria poiché contiene la willDisplayfunzione sopra indicata . Spero di poter salvare qualcuno nella stessa lotta.
MJQZ1347,

Grazie per la risposta rapida. Nel mio caso stavo avendo un comportamento SUPER strano delle celle che andavano fuori servizio al ricaricarsi quando la vista della tabella veniva fatta scorrere verso / vicino al fondo. Lo userò d'ora in poi ogni volta che avrò celle auto-dimensionanti.
Trev14

Funziona perfettamente in Swift 4.2
Adam S.

Un salvavita. Molto utile quando si tenta di aggiungere più elementi nell'origine dati. Impedisce il salto delle celle appena aggiunte al centro dello schermo.
Philip Borbon,

38

Il salto è a causa di una cattiva altezza stimata. Maggiore è il valore stimato di RowHeight dall'altezza effettiva, più la tabella può saltare quando viene ricaricata, soprattutto più in basso è stata fatta scorrere. Questo perché le dimensioni stimate della tabella differiscono radicalmente dalle dimensioni effettive, costringendo la tabella a regolare le dimensioni del contenuto e l'offset. Quindi l'altezza stimata non dovrebbe essere un valore casuale ma vicino a quello che pensi sarà l'altezza. Ho anche sperimentato quando ho impostato UITableViewAutomaticDimension se le tue celle sono dello stesso tipo allora

func viewDidLoad() {
     super.viewDidLoad()
     tableView.estimatedRowHeight = 100//close to your cell height
}

se hai varietà di cellule in diverse sezioni, penso che sia il posto migliore

func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
     //return different sizes for different cells if you need to
     return 100
}

2
grazie, è esattamente il motivo per cui il mio tableView era così nervoso.
Louis de Decker,

1
Una vecchia risposta, ma è ancora attuale a partire dal 2018. A differenza di tutte le altre risposte, questa suggerisce di impostare una volta stimato ViewHeighDidLoad, che aiuta quando le celle hanno la stessa o molto simile altezza. Grazie. A proposito, in alternativa esimatedRowHeight può essere impostato tramite Interface Builder in Impostazioni dimensioni> Visualizzazione tabella> Stima.
Vitalii,

fornito un'altezza stimata più accurata mi ha aiutato. Avevo anche uno stile di visualizzazione di tabelle raggruppate in più sezioni e ho dovuto implementarlotableView(_:estimatedHeightForHeaderInSection:)
nteissler

25

La risposta di @Igor funziona bene in questo caso,Swift-4codice di esso.

// declaration & initialization  
var cellHeightsDictionary: [IndexPath: CGFloat] = [:]  

nei seguenti metodi di UITableViewDelegate

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
  // print("Cell height: \(cell.frame.size.height)")
  self.cellHeightsDictionary[indexPath] = cell.frame.size.height
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
  if let height =  self.cellHeightsDictionary[indexPath] {
    return height
  }
  return UITableView.automaticDimension
}

6
Come gestire l'inserimento / eliminazione di righe con questa soluzione? TableView salta, poiché i dati del dizionario non sono effettivi.
Alexey Chekanov,

1
funziona alla grande! specialmente sull'ultima cella quando si ricarica la riga.
Ning

19

Ho provato tutte le soluzioni alternative sopra, ma nulla ha funzionato.

Dopo aver trascorso ore e aver affrontato tutte le possibili frustrazioni, ho trovato un modo per risolvere questo problema. Questa soluzione è un salvatore di vita! Ha funzionato come un fascino!

Swift 4

let lastContentOffset = tableView.contentOffset
tableView.beginUpdates()
tableView.endUpdates()
tableView.layer.removeAllAnimations()
tableView.setContentOffset(lastContentOffset, animated: false)

L'ho aggiunto come estensione, per rendere il codice più pulito ed evitare di scrivere tutte queste righe ogni volta che voglio ricaricare.

extension UITableView {

    func reloadWithoutAnimation() {
        let lastScrollOffset = contentOffset
        beginUpdates()
        endUpdates()
        layer.removeAllAnimations()
        setContentOffset(lastScrollOffset, animated: false)
    }
}

finalmente ..

tableView.reloadWithoutAnimation()

O potresti effettivamente aggiungere queste righe nel tuo UITableViewCell awakeFromNib()metodo

layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale

e fare normale reloadData()


1
Come fa questo ricaricare? Si chiami essa reloadWithoutAnimation, ma dov'è la reloadparte?
matt

@matt potresti chiamare tableView.reloadData()prima e poi tableView.reloadWithoutAnimation()funziona ancora.
Srujan Simha,

Grande! Nessuno dei precedenti non ha funzionato per me. Anche tutte le altezze e le altezze stimate sono totalmente uguali. Interessante.
TY Kucuk,

1
Non lavorare per me. Si arresta in modo anomalo su tableView.endUpdates (). Qualcuno può aiutarmi!
Kakashi,

12

Uso più modi per risolverlo:

Per il controller di visualizzazione:

var cellHeights: [IndexPath : CGFloat] = [:]


func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    cellHeights[indexPath] = cell.frame.size.height
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return cellHeights[indexPath] ?? 70.0 
}

come estensione per UITableView

extension UITableView {
  func reloadSectionWithouAnimation(section: Int) {
      UIView.performWithoutAnimation {
          let offset = self.contentOffset
          self.reloadSections(IndexSet(integer: section), with: .none)
          self.contentOffset = offset
      }
  }
}

Il risultato è

tableView.reloadSectionWithouAnimation(section: indexPath.section)

1
La chiave per me era l'implementazione della sua estensione UITableView qui. Molto intelligente. Grazie rastislv
BennyTheNerd,

Funziona perfettamente ma ha un solo inconveniente: si perde l'animazione quando si inseriscono intestazione, piè di pagina o riga.
Soufian Hossam,

Dove verrebbe chiamato reloadSectionWithouAnimation? Quindi, ad esempio, gli utenti possono pubblicare un'immagine nella mia app (come Instagram); Posso ottenere il ridimensionamento delle immagini, ma nella maggior parte dei casi devo scorrere la cella della tabella dal ghiaione affinché ciò accada. Voglio che la cella abbia la dimensione corretta una volta che la tabella passa attraverso reloadData.
Luke Irvin,

11

Mi sono imbattuto in questo oggi e ho osservato:

  1. È solo iOS 8, in effetti.
  2. Sostituire cellForRowAtIndexPathnon aiuta.

La correzione era in realtà piuttosto semplice:

Sostituisci estimatedHeightForRowAtIndexPathe assicurati che restituisca i valori corretti.

Con questo, tutti gli strani strani jitter e salti nelle mie UITableViews si sono fermati.

NOTA: in realtà conosco le dimensioni delle mie celle. Esistono solo due valori possibili. Se le tue celle sono veramente di dimensioni variabili, potresti voler memorizzare nella cache il cell.bounds.size.heightdatableView:willDisplayCell:forRowAtIndexPath:


2
Risolto il problema con l'override del metodo stimatoHeightForRowAtIndexPath con un valore elevato, ad esempio 300f
Flappy,

1
@Flappy è interessante come funziona la soluzione fornita da te ed è più breve di altre tecniche suggerite. Prendi in considerazione di pubblicarlo come risposta.
Rohan Sanap,

9

Puoi infatti ricaricare solo determinate righe usando reloadRowsAtIndexPaths, ad esempio:

tableView.reloadRowsAtIndexPaths(indexPathArray, withRowAnimation: UITableViewRowAnimation.None)

Ma, in generale, potresti anche animare i cambiamenti di altezza delle celle della tabella in questo modo:

tableView.beginUpdates()
tableView.endUpdates()

Ho provato il metodo beginUpdates / endUpdates, ma questo riguarda solo le righe visibili della mia tabella. Ho ancora il problema quando scorro verso l'alto.
David

@David Probabilmente perché stai utilizzando le altezze di riga stimate.
Lyndsey Scott,

Devo sbarazzarmi di EstimatedRowHeights e sostituirlo invece con beginUpdates e endUpdates?
David

@David Non dovresti "sostituire" nulla, ma dipende davvero dal comportamento desiderato ... Se vuoi usare l'altezza delle righe stimata e ricaricare gli indici sotto l'attuale porzione visibile della tabella, puoi farlo come Ho detto usando reloadRowsAtIndexPaths
Lyndsey Scott il

Uno dei miei problemi nel provare il metodo reladRowsAtIndexPaths è che sto implementando lo scrolling infinito, quindi quando sto ricaricandoData è perché ho appena aggiunto altre 15 righe al dataSource. Ciò significa che gli IndexPath per quelle righe non esistono ancora in UITableView
David

3

Ecco una versione un po 'più breve:

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    return self.cellHeightsDictionary[indexPath] ?? UITableViewAutomaticDimension
}

3

Sostituzione del metodo stimatoHeightForRowAtIndexPath con un valore elevato, ad esempio 300f

Questo dovrebbe risolvere il problema :)


2

C'è un bug che credo sia stato introdotto in iOS11.

Cioè quando si esegue un reloadtableView contentOffSetviene inaspettatamente modificato. In realtà contentOffsetnon dovrebbe cambiare dopo un ricaricamento. Tende a succedere a causa di errori di calcolo diUITableViewAutomaticDimension

Devi salvare il tuo contentOffSete ripristinarlo al valore salvato una volta terminato il ricaricamento.

func reloadTableOnMain(with offset: CGPoint = CGPoint.zero){

    DispatchQueue.main.async { [weak self] () in

        self?.tableView.reloadData()
        self?.tableView.layoutIfNeeded()
        self?.tableView.contentOffset = offset
    }
}

Come lo usi?

someFunctionThatMakesChangesToYourDatasource()
let offset = tableview.contentOffset
reloadTableOnMain(with: offset)

Questa risposta è stata derivata da qui


2

Questo ha funzionato per me in Swift4:

extension UITableView {

    func reloadWithoutAnimation() {
        let lastScrollOffset = contentOffset
        reloadData()
        layoutIfNeeded()
        setContentOffset(lastScrollOffset, animated: false)
    }
}

1

Nessuna di queste soluzioni ha funzionato per me. Ecco cosa ho fatto con Swift 4 e Xcode 10.1 ...

In viewDidLoad (), dichiara l'altezza della riga dinamica della tabella e crea vincoli corretti nelle celle ...

tableView.rowHeight = UITableView.automaticDimension

Sempre in viewDidLoad (), registra tutti i pennini delle celle di TableView in tableview in questo modo:

tableView.register(UINib(nibName: "YourTableViewCell", bundle: nil), forCellReuseIdentifier: "YourTableViewCell")
tableView.register(UINib(nibName: "YourSecondTableViewCell", bundle: nil), forCellReuseIdentifier: "YourSecondTableViewCell")
tableView.register(UINib(nibName: "YourThirdTableViewCell", bundle: nil), forCellReuseIdentifier: "YourThirdTableViewCell")

In tableView heightForRowAt, restituisce l'altezza uguale all'altezza di ciascuna cella in indexPath.row ...

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {

    if indexPath.row == 0 {
        let cell = Bundle.main.loadNibNamed("YourTableViewCell", owner: self, options: nil)?.first as! YourTableViewCell
        return cell.layer.frame.height
    } else if indexPath.row == 1 {
        let cell = Bundle.main.loadNibNamed("YourSecondTableViewCell", owner: self, options: nil)?.first as! YourSecondTableViewCell
        return cell.layer.frame.height
    } else {
        let cell = Bundle.main.loadNibNamed("YourThirdTableViewCell", owner: self, options: nil)?.first as! YourThirdTableViewCell
        return cell.layer.frame.height
    } 

}

Ora fornisci un'altezza di riga stimata per ogni cella nella tabella Visualizza stimaHeightForRowAt. Sii preciso come puoi ...

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {

    if indexPath.row == 0 {
        return 400 // or whatever YourTableViewCell's height is
    } else if indexPath.row == 1 {
        return 231 // or whatever YourSecondTableViewCell's height is
    } else {
        return 216 // or whatever YourThirdTableViewCell's height is
    } 

}

Dovrebbe funzionare ...

Non ho avuto bisogno di salvare e impostare contentOffset quando ho chiamato tableView.reloadData ()


1

Ho 2 diverse altezze di cella.

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
        return Helper.makeDeviceSpecificCommonSize(cellHeight)
    }

Dopo aver aggiunto stimeHeightForRowAt , non c'era più il salto.

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
    return Helper.makeDeviceSpecificCommonSize(cellHeight)
}

0

Prova a chiamare cell.layoutSubviews()prima di restituire la cella func cellForRowAtIndexPath(_ indexPath: NSIndexPath) -> UITableViewCell?. È noto bug in iOS8.


0

È possibile utilizzare quanto segue in ViewDidLoad()

tableView.estimatedRowHeight = 0     // if have just tableViewCells <br/>

// use this if you have tableview Header/footer <br/>
tableView.estimatedSectionFooterHeight = 0 <br/>
tableView.estimatedSectionHeaderHeight = 0

0

Ho avuto questo comportamento saltellante e inizialmente sono stato in grado di mitigarlo impostando l'altezza esatta dell'intestazione stimata (perché avevo solo 1 possibile vista dell'intestazione), tuttavia i salti hanno iniziato a verificarsi all'interno delle intestazioni in modo specifico, senza influenzare più l'intero tavolo.

Seguendo le risposte qui, ho avuto la sensazione che fosse correlato alle animazioni, quindi ho scoperto che la vista della tabella era all'interno di una vista pila, e talvolta chiamavamo stackView.layoutIfNeeded()all'interno di un blocco di animazione. La mia soluzione finale è stata quella di assicurarmi che questa chiamata non avvenisse a meno che "veramente" non fosse necessario, poiché il layout "se necessario" aveva comportamenti visivi in ​​quel contesto anche quando "non necessario".


0

Ho avuto lo stesso problema. Ho avuto l'impaginazione e ricaricato i dati senza animazione, ma non ha aiutato lo scorrimento per evitare il salto. Ho diverse dimensioni di IPhones, lo scorrimento non era nervoso su iPhone8 ma era nervoso su iPhone7 +

Ho applicato le seguenti modifiche alla funzione viewDidLoad :

    self.myTableView.estimatedRowHeight = 0.0
    self.myTableView.estimatedSectionFooterHeight = 0
    self.myTableView.estimatedSectionHeaderHeight = 0

e il mio problema è stato risolto. Spero aiuti anche te.


0

Uno degli approcci per risolvere questo problema che ho riscontrato è

CATransaction.begin()
UIView.setAnimationsEnabled(false)
CATransaction.setCompletionBlock {
   UIView.setAnimationsEnabled(true)
}
tableView.reloadSections([indexPath.section], with: .none)
CATransaction.commit()

-2

In realtà ho scoperto che se usi reloadRowscausando un problema di salto. Quindi dovresti provare a usare in reloadSectionsquesto modo:

UIView.performWithoutAnimation {
    tableView.reloadSections(NSIndexSet(index: indexPath.section) as IndexSet, with: .none)
}
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.