reloadData () von UITableView mit dynamischen Zellenhöhen verursacht ein nervöses Scrollen


142

Ich bin der Meinung, dass dies ein häufiges Problem sein könnte, und habe mich gefragt, ob es eine gemeinsame Lösung dafür gibt.

Grundsätzlich hat mein UITableView dynamische Zellenhöhen für jede Zelle. Wenn ich nicht oben in UITableView bin und ich tableView.reloadData(), wird das Scrollen nach oben nervös.

Ich glaube, dies liegt an der Tatsache, dass UITableView die Höhe für jede Zelle, die sichtbar wird, neu berechnet, da ich beim Scrollen Daten hochgeladen habe. Wie kann ich das abmildern oder wie lade ich Daten nur von einem bestimmten IndexPath bis zum Ende der UITableView neu?

Wenn ich es schaffe, ganz nach oben zu scrollen, kann ich wieder nach unten und dann nach oben scrollen, kein Problem, ohne zu springen. Dies liegt höchstwahrscheinlich daran, dass die UITableViewCell-Höhen bereits berechnet wurden.


Ein paar Dinge ... (1) Ja, Sie können bestimmte Zeilen definitiv mit neu laden reloadRowsAtIndexPaths. Aber (2) was meinst du mit "nervös" und (3) hast du eine geschätzte Zeilenhöhe eingestellt? (Ich versuche nur herauszufinden, ob es eine bessere Lösung gibt, mit der Sie die Tabelle dynamisch aktualisieren können.)
Lyndsey Scott

@LyndseyScott, ja, ich habe eine geschätzte Zeilenhöhe festgelegt. Mit nervös meine ich, dass sich die Zeilen beim Scrollen nach oben verschieben. Ich glaube, das liegt daran, dass ich eine geschätzte Zeilenhöhe von 128 festgelegt habe. Wenn ich dann nach oben scrolle, sind alle meine Beiträge oben in der UITableView kleiner, sodass die Höhe kleiner wird und meine Tabelle springt. Ich denke darüber nach, reloadRowsAtIndexPaths von der Zeile xbis zur letzten Zeile in meiner TableView auszuführen. Da ich jedoch neue Zeilen einfüge, funktioniert dies nicht. Ich kann nicht wissen, wie das Ende meiner Tabellenansicht vor dem erneuten Laden aussehen wird die Daten.
David

2
@LyndseyScott Ich kann das Problem immer noch nicht lösen. Gibt es eine gute Lösung?
Rad

1
Haben Sie jemals eine Lösung für dieses Problem gefunden? Ich habe genau das gleiche Problem wie in Ihrem Video.
user3344977

1
Keine der folgenden Antworten hat bei mir funktioniert.
Srujan Simha

Antworten:


220

Um ein Springen zu verhindern, sollten Sie beim Laden die Höhe der Zellen speichern und den genauen Wert angeben in tableView:estimatedHeightForRowAtIndexPath:

Schnell:

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
}

Ziel 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
Danke, du rettest wirklich meinen Tag :) Funktioniert auch in objc
Artem Z.

3
Vergessen Sie nicht zu initialisieren cellHeightsDictionary: cellHeightsDictionary = [NSMutableDictionary dictionary];
Gerharbo

1
estimatedHeightForRowAtIndexPath:Gibt einen doppelten Wert zurück, kann einen *** Assertion failure in -[UISectionRowData refreshWithSection:tableView:tableViewRowData:]Fehler verursachen. Um es return floorf(height.floatValue);stattdessen zu beheben .
Liushuaikobe

Hallo @lgor, ich habe das gleiche Problem und versuche, Ihre Lösung zu implementieren. Das Problem, das ich erhalte, ist EstimatedHeightForRowAtIndexPath, das vor willDisplayCell aufgerufen wird. Daher wird die Zellenhöhe nicht berechnet, wenn EstimatedHeightForRowAtIndexPath aufgerufen wird. Irgendeine Hilfe?
Madhuri

1
Die effektiven Höhen von @Madhuri sollten in "heightForRowAtIndexPath" berechnet werden, das für jede Zelle auf dem Bildschirm unmittelbar vor willDisplayCell aufgerufen wird. Dadurch wird die Höhe im Wörterbuch für die spätere Verwendung in geschätzterRowHeight (beim erneuten Laden der Tabelle) festgelegt.
Donnit

108

Swift 3-Version der akzeptierten Antwort.

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 
}

Danke, das hat super funktioniert! Tatsächlich konnte ich meine Implementierung von entfernen func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {, dies erledigt alle Höhenberechnungen, die ich brauche.
Natalia

Nachdem ich viele Stunden mit hartnäckigem Springen zu kämpfen hatte, stellte ich fest, dass ich vergessen hatte UITableViewDelegate, meine Klasse zu erweitern. Die Anpassung an dieses Protokoll ist erforderlich, da es die oben gezeigte willDisplayFunktion enthält. Ich hoffe, ich kann jemandem den gleichen Kampf ersparen.
MJQZ1347

Vielen Dank für die schnelle Antwort. In meinem Fall hatte ich ein SUPER seltsames Verhalten von Zellen, die beim Neuladen nicht mehr in Ordnung waren, als die Tabellenansicht nach / in der Nähe des unteren Bereichs gescrollt wurde. Ich werde dies von nun an verwenden, wenn ich Zellen mit Selbstgröße habe.
Trev14

Funktioniert perfekt in Swift 4.2
Adam S.

Ein Lebensretter. Dies ist hilfreich, wenn Sie versuchen, weitere Elemente zur Datenquelle hinzuzufügen. Verhindert das Springen neu hinzugefügter Zellen in die Mitte des Bildschirms.
Philip Borbon

38

Der Sprung ist wegen einer schlechten geschätzten Höhe. Je mehr sich die geschätzte Zeilenhöhe von der tatsächlichen Höhe unterscheidet, desto mehr kann die Tabelle springen, wenn sie neu geladen wird, insbesondere je weiter nach unten gescrollt wurde. Dies liegt daran, dass sich die geschätzte Größe der Tabelle radikal von der tatsächlichen Größe unterscheidet und die Tabelle gezwungen ist, ihre Inhaltsgröße und ihren Versatz anzupassen. Die geschätzte Höhe sollte also kein zufälliger Wert sein, sondern in der Nähe dessen, was Sie denken, dass die Höhe sein wird. Ich habe auch erfahren, wann ich festgelegt habe, UITableViewAutomaticDimension ob Ihre Zellen dann vom gleichen Typ sind

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

Wenn Sie verschiedene Zellen in verschiedenen Abschnitten haben, dann denke ich, ist der bessere Ort

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

2
danke, genau deshalb war mein tableView so nervös.
Louis de Decker

1
Eine alte Antwort, die jedoch ab 2018 noch aktuell ist. Im Gegensatz zu allen anderen Antworten wird in dieser Antwort vorgeschlagen, geschätzte Zeilenhöhe einmal in viewDidLoad festzulegen. Dies ist hilfreich, wenn Zellen dieselbe oder eine sehr ähnliche Höhe haben. Danke. Übrigens kann alternativ esimatedRowHeight über den Interface Builder unter Größeninspektor> Tabellenansicht> Schätzung festgelegt werden.
Vitalii

vorausgesetzt, eine genauere geschätzte Höhe hat mir geholfen. Ich hatte auch einen mehrteiligen gruppierten Tabellenansichtsstil und musste implementierentableView(_:estimatedHeightForHeaderInSection:)
nteissler

25

@Igor Antwort funktioniert in diesem Fall gut,Swift-4Code davon.

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

in folgenden Methoden von 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
Wie gehe ich mit dem Einfügen / Löschen von Zeilen mit dieser Lösung um? TableView springt, da die Wörterbuchdaten nicht aktuell sind.
Alexey Chekanov

1
funktioniert super! besonders in der letzten Zelle beim erneuten Laden der Zeile.
Ning

19

Ich habe alle oben genannten Problemumgehungen ausprobiert, aber nichts hat funktioniert.

Nachdem Sie Stunden damit verbracht hatten, alle möglichen Frustrationen durchzugehen, fanden Sie einen Weg, dies zu beheben. Diese Lösung ist ein Lebensretter! Lief wie am Schnürchen!

Swift 4

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

Ich habe es als Erweiterung hinzugefügt, damit der Code sauberer aussieht und nicht jedes Mal, wenn ich neu laden möchte, alle diese Zeilen geschrieben werden.

extension UITableView {

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

schließlich ..

tableView.reloadWithoutAnimation()

ODER Sie könnten diese Zeile tatsächlich zu Ihrer UITableViewCell awakeFromNib()Methode hinzufügen

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

und normal machen reloadData()


1
Wie wird das Nachladen durchgeführt? Du nennst es reloadWithoutAnimationaber wo ist der reloadTeil?
Matt

@matt könnte man tableView.reloadData()zuerst anrufen und dann tableView.reloadWithoutAnimation()funktioniert es immer noch.
Srujan Simha

Toll! Keiner der oben genannten Punkte hat bei mir auch nicht funktioniert. Sogar alle Höhen und geschätzten Höhen sind völlig gleich. Interessant.
TY Kucuk

1
Arbeite nicht für mich. Es ist ein Absturz bei tableView.endUpdates (). Kann mir jemand helfen!
Kakashi

12

Ich benutze mehr Möglichkeiten, um das Problem zu beheben:

Für View Controller:

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 
}

als Erweiterung für UITableView

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

Das Ergebnis ist

tableView.reloadSectionWithouAnimation(section: indexPath.section)

1
Der Schlüssel für mich war die Implementierung seiner UITableView-Erweiterung hier. Sehr schlau. Vielen Dank rastislv
BennyTheNerd

Funktioniert perfekt, hat aber nur einen Nachteil: Sie verlieren die Animation beim Einfügen von Kopf-, Fuß- oder Zeilen.
Soufian Hossam

Wo würde reloadSectionWithouAnimation aufgerufen werden? So können Benutzer beispielsweise ein Bild in meiner App veröffentlichen (z. B. Instagram). Ich kann die Größe der Bilder ändern, aber in den meisten Fällen muss ich die Tabellenzelle aus dem Geröll scrollen, damit dies geschieht. Ich möchte, dass die Zelle die richtige Größe hat, sobald die Tabelle reloadData durchläuft.
Luke Irvin

11

Ich bin heute darauf gestoßen und habe Folgendes beobachtet:

  1. Es ist in der Tat nur iOS 8.
  2. Überschreiben cellForRowAtIndexPathhilft nicht.

Das Update war eigentlich ziemlich einfach:

Überschreiben Sie estimatedHeightForRowAtIndexPathund stellen Sie sicher, dass die richtigen Werte zurückgegeben werden.

Damit hat alles seltsame Zittern und Herumspringen in meinen UITableViews aufgehört.

HINWEIS: Ich kenne die Größe meiner Zellen. Es gibt nur zwei mögliche Werte. Wenn Ihre Zellen wirklich eine variable Größe haben, möchten Sie möglicherweise die cell.bounds.size.heightvon zwischenspeicherntableView:willDisplayCell:forRowAtIndexPath:


2
Es wurde behoben, dass die geschätzte HeightForRowAtIndexPath-Methode mit einem hohen Wert überschrieben wurde, z. B. 300f
Flappy

1
@Flappy es ist interessant, wie die von Ihnen bereitgestellte Lösung funktioniert und ist kürzer als andere vorgeschlagene Techniken. Erwägen Sie, es als Antwort zu veröffentlichen.
Rohan Sanap

8

Sie können tatsächlich nur bestimmte Zeilen neu laden, indem Sie reloadRowsAtIndexPathsFolgendes verwenden:

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

Im Allgemeinen können Sie aber auch Änderungen der Tabellenzellenhöhe wie folgt animieren:

tableView.beginUpdates()
tableView.endUpdates()

Ich habe die Methode beginUpdates / endUpdates ausprobiert, aber das betrifft nur die sichtbaren Zeilen meiner Tabelle. Ich habe immer noch das Problem, wenn ich nach oben scrolle.
David

@ David Wahrscheinlich, weil Sie geschätzte Zeilenhöhen verwenden.
Lyndsey Scott

Sollte ich meine EstimatedRowHeights loswerden und sie stattdessen durch beginUpdates und endUpdates ersetzen?
David

@ David Sie würden nichts "ersetzen", aber es hängt wirklich vom gewünschten Verhalten ab ... Wenn Sie die geschätzte Zeilenhöhe verwenden und die Indizes unter dem aktuell sichtbaren Teil der Tabelle neu laden möchten, können Sie dies wie folgt tun Ich sagte mit reloadRowsAtIndexPaths
Lyndsey Scott

Eines meiner Probleme beim Ausprobieren der reladRowsAtIndexPaths-Methode ist, dass ich unendliches Scrollen implementiere. Wenn ich also Daten neu lade, liegt dies daran, dass ich der dataSource gerade 15 weitere Zeilen hinzugefügt habe. Dies bedeutet, dass die indexPaths für diese Zeilen noch nicht in der UITableView
David

3

Hier ist eine etwas kürzere Version:

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

3

Überschreiben der geschätzten HeightForRowAtIndexPath-Methode mit einem hohen Wert, z. B. 300f

Dies sollte das Problem beheben :)


2

Es gibt einen Fehler, von dem ich glaube, dass er in iOS11 eingeführt wurde.

In reloaddiesem Fall contentOffSetwird die tableView unerwartet geändert. In der Tat contentOffsetsollte sich nach einem Neuladen nicht ändern. Es kann aufgrund von Fehlkalkulationen von passierenUITableViewAutomaticDimension

Sie müssen Ihre speichern contentOffSetund nach dem erneuten Laden auf Ihren gespeicherten Wert zurücksetzen.

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

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

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

Wie benutzt du es?

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

Diese Antwort wurde von hier abgeleitet


2

Dieser hat in Swift4 für mich gearbeitet:

extension UITableView {

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

1

Keine dieser Lösungen hat bei mir funktioniert. Folgendes habe ich mit Swift 4 & Xcode 10.1 gemacht ...

Deklarieren Sie in viewDidLoad () die dynamische Zeilenhöhe der Tabelle und erstellen Sie korrekte Einschränkungen in Zellen ...

tableView.rowHeight = UITableView.automaticDimension

Registrieren Sie auch in viewDidLoad () alle Ihre tableView-Zellennibs in tableview wie folgt:

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")

Geben Sie in tableView heightForRowAt die Höhe zurück, die der Höhe jeder Zelle bei indexPath.row ... entspricht.

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
    } 

}

Geben Sie nun eine geschätzte Zeilenhöhe für jede Zelle in tableView EstimatedHeightForRowAt an. Sei genau wie du kannst ...

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
    } 

}

Das sollte funktionieren...

Ich musste contentOffset beim Aufrufen von tableView.reloadData () nicht speichern und festlegen.


1

Ich habe 2 verschiedene Zellenhöhen.

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

Nachdem ich schätzungsweise HeightForRowAt hinzugefügt hatte , gab es kein Springen mehr.

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

0

Versuchen Sie anzurufen, cell.layoutSubviews()bevor Sie die Zelle zurückgeben func cellForRowAtIndexPath(_ indexPath: NSIndexPath) -> UITableViewCell?. Es ist ein Fehler in iOS8 bekannt.


0

Sie können Folgendes in verwenden 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

Ich hatte dieses Sprungverhalten und konnte es zunächst durch Festlegen der genauen geschätzten Kopfhöhe abmildern (da ich nur eine mögliche Kopfzeilensicht hatte). Die Sprünge begannen dann jedoch speziell innerhalb der Kopfzeilen und wirkten sich nicht mehr auf die gesamte Tabelle aus.

Nach den Antworten hier hatte ich den Hinweis, dass es sich um Animationen handelt, und stellte fest, dass sich die Tabellenansicht in einer Stapelansicht befand, und manchmal riefen wir stackView.layoutIfNeeded()in einem Animationsblock auf. Meine endgültige Lösung bestand darin, sicherzustellen, dass dieser Aufruf nur dann erfolgt, wenn "wirklich" benötigt wird, da das Layout "falls erforderlich" in diesem Kontext visuelles Verhalten aufweist, selbst wenn "nicht benötigt".


0

Ich hatte das gleiche Problem. Ich hatte Paginierung und erneutes Laden von Daten ohne Animation, aber es half der Schriftrolle nicht, das Springen zu verhindern. Ich habe eine andere Größe von IPhones, die Schriftrolle war auf dem iPhone 8 nicht nervös, aber auf dem iPhone 7 + war sie nervös

Ich habe folgende Änderungen an der viewDidLoad- Funktion vorgenommen:

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

und mein Problem gelöst. Ich hoffe es hilft dir auch.


0

Einer der Ansätze zur Lösung dieses Problems, den ich gefunden habe, ist

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

-2

Eigentlich habe ich festgestellt, wenn Sie reloadRowsein Sprungproblem verursachen. Dann sollten Sie versuchen, reloadSectionswie folgt zu verwenden :

UIView.performWithoutAnimation {
    tableView.reloadSections(NSIndexSet(index: indexPath.section) as IndexSet, with: .none)
}
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.