webentwicklung-frage-antwort-db.com.de

reloadData () von UITableView mit dynamischen Zellenhöhen verursacht einen sprunghaften Bildlauf

Ich bin der Meinung, dass dies ein allgemeines Problem sein könnte, und fragte mich, ob es eine gemeinsame Lösung dafür gab.

Grundsätzlich verfügt mein UITableView über dynamische Zellenhöhen für jede Zelle. Wenn ich mich nicht an der Spitze der UITableView befinde und ich tableView.reloadData(), wird das Scrollen nach oben sprunghaft. 

Ich glaube, das liegt an der Tatsache, dass UITableView die Höhe für jede in Sichtbarkeit kommende Zelle neu berechnet, weil ich Daten neu geladen habe, während ich hoch scrolle. Wie kann ich das abmildern, oder wie kann ich ReloadData nur von einem bestimmten IndexPath bis zum Ende der UITableView laden? 

Wenn ich es schaffe, ganz nach oben zu scrollen, kann ich nach unten und dann nach oben scrollen, kein Problem ohne Springen. Dies ist höchstwahrscheinlich darauf zurückzuführen, dass die Höhen von UITableViewCell bereits berechnet wurden.

80
David

Um Sprünge zu vermeiden, sollten Sie die Höhe der Zellen beim Laden speichern und einen genauen Wert in tableView:estimatedHeightForRowAtIndexPath angeben:

// declare cellHeightsDictionary
NSMutableDictionary *cellHeightsDictionary;

// initialize in code (thanks to @Gerharbo)
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;
}
141
Igor

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 
}
74
Casey Wagner

Der Sprung ist wegen einer schlechten geschätzten Höhe. Je mehr die geschätzteRowHeight von der tatsächlichen Höhe abweicht, desto mehr springt die Tabelle, wenn sie neu geladen wird, insbesondere, je weiter unten gescrollt wurde. Dies liegt daran, dass die geschätzte Größe der Tabelle radikal von der tatsächlichen Größe abweicht, sodass die Tabelle die Größe und den Versatz des Inhalts anpassen muss. Die geschätzte Höhe sollte also kein Zufallswert sein, sollte aber nahe an dem liegen, was Ihrer Meinung nach die Höhe ist Sein. Ich habe auch erfahren, wenn ich UITableViewAutomaticDimension Setze, wenn Ihre Zellen dann vom gleichen Typ sind 

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

wenn Sie unterschiedliche Zellen in verschiedenen Abschnitten haben, 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
        }
25
Krishna Kishore

@IgorAntwort funktioniert in diesem Fall gut, Swift-4-Code 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 UITableViewAutomaticDimension
}
21
Kiran Jasvanee

Ich bin heute darauf gestoßen und habe beobachtet:

  1. Es ist tatsächlich nur iOS 8.
  2. Das Überschreiben von cellForRowAtIndexPath hilft nicht.

Das Update war eigentlich ziemlich einfach:

Überschreiben Sie estimatedHeightForRowAtIndexPath und stellen Sie sicher, dass die korrekten Werte zurückgegeben werden.

Damit ist alles seltsame Wackeln und Springen in meinen UITableViews gestoppt.

HINWEIS: Ich weiß eigentlich, wie groß meine Zellen sind. Es gibt nur zwei mögliche Werte. Wenn Ihre Zellen wirklich variable Größe haben, möchten Sie möglicherweise den cell.bounds.size.height von tableView:willDisplayCell:forRowAtIndexPath: zwischenspeichern.

8
MarcWan

Sie können tatsächlich nur bestimmte Zeilen mit reloadRowsAtIndexPaths laden, zB:

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

Im Allgemeinen können Sie jedoch auch die Änderungen der Tabellenzellhöhe wie folgt animieren:

tableView.beginUpdates()
tableView.endUpdates()
6
Lyndsey Scott

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

Nachdem ich Stunden verbracht hatte und alle möglichen Frustrationen durchgemacht hatte, fand ich 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 Zeilen geschrieben wird.

extension UITableView {

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

endlich ..

tableView.reloadWithoutAnimation()
5
Srujan Simha

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

Für den 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

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)
2
rastislv

Hier ist eine etwas kürzere Version:

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

Dieser hat in Swift4 für mich gearbeitet:

extension UITableView {

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

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 geschätztHoheForRowAt hinzugefügt habe, 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
sabiland

In ViewDidLoad() können Sie Folgendes verwenden:

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
Vid

Keine dieser Lösungen funktionierte für mich. Hier ist was ich mit Swift 4 & Xcode 10.1 gemacht habe ...

In viewDidLoad () deklarieren Sie 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-Zellenspitzen wie folgt in tableview:

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 jeder Zelle in indexPath.row zurück.

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 EstimHeightForRowAt an. Seien Sie genau wie Sie können ...

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...

Beim Aufruf von tableView.reloadData () musste ich contentOffset nicht speichern und festlegen.

0
Michael Colonna

Überschreiben der EstimatedHeightForRowAtIndexPath-Methode mit einem hohen Wert, z. B. 300f

Dies sollte das Problem beheben :)

0
Flappy

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

Wenn Sie eine reload ausführen, wird die tableView contentOffSet unerwartet geändert. Tatsächlich sollte sich contentOffset nach einem Neuladen nicht ändern. Dies geschieht aufgrund von Fehlberechnungen von UITableViewAutomaticDimension

Sie müssen Ihre contentOffSet speichern und nach dem Laden wieder auf Ihren gespeicherten Wert setzen. 

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.

0
Honey

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

0
CrimeZone