Ruckeliges Scrollen nach dem Aktualisieren von UITableViewCell mit UITableViewAutomaticDimension

78

Ich erstelle eine App mit einer Feed-Ansicht für vom Benutzer eingereichte Beiträge. Diese Ansicht hat eine UITableViewmit einer benutzerdefinierten UITableViewCellImplementierung. In dieser Zelle habe ich eine andere UITableViewzum Anzeigen von Kommentaren. Das Wesentliche ist ungefähr so:

Feed TableView
  PostCell
    Comments (TableView)
      CommentCell
  PostCell
    Comments (TableView)
      CommentCell
      CommentCell
      CommentCell
      CommentCell
      CommentCell

Der erste Feed wird mit 3 Kommentaren zur Vorschau heruntergeladen. Wenn jedoch weitere Kommentare vorhanden sind oder der Benutzer einen Kommentar hinzufügt oder löscht, möchte ich den vorhandenen PostCellPlatz in der Feed-Tabellenansicht aktualisieren, indem ich ihn CommentCellszur Kommentartabelle hinzufüge oder daraus entferne der PostCell. Ich benutze derzeit den folgenden Helfer, um dies zu erreichen:

// (PostCell.swift) Handle showing/hiding comments
func animateAddOrDeleteComments(startRow: Int, endRow: Int, operation: CellOperation) {
  let table = self.superview?.superview as UITableView

  // "table" is outer feed table
  // self is the PostCell that is updating it's comments
  // self.comments is UITableView for displaying comments inside of the PostCell
  table.beginUpdates()
  self.comments.beginUpdates()

  // This function handles inserting/removing/reloading a range of comments
  // so we build out an array of index paths for each row that needs updating
  var indexPaths = [NSIndexPath]()
  for var index = startRow; index <= endRow; index++ {
    indexPaths.append(NSIndexPath(forRow: index, inSection: 0))
  }

  switch operation {
  case .INSERT:
    self.comments.insertRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
  case .DELETE:
    self.comments.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
  case .RELOAD:
    self.comments.reloadRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
  }

  self.comments.endUpdates()
  table.endUpdates()

  // trigger a call to updateConstraints so that we can update the height constraint 
  // of the comments table to fit all of the comments
  self.setNeedsUpdateConstraints()
}

override func updateConstraints() {
  super.updateConstraints()
  self.commentsHeight.constant = self.comments.sizeThatFits(UILayoutFittingCompressedSize).height
}

Dies führt das Update einwandfrei durch. Der Beitrag wird an Ort und Stelle aktualisiert, wobei Kommentare PostCellwie erwartet innerhalb des Beitrags hinzugefügt oder entfernt werden . Ich verwende die automatische Größenanpassung PostCellsin der Feed-Tabelle. Die Kommentartabelle des wird PostCellerweitert, um alle Kommentare anzuzeigen, aber die Animation ist etwas ruckelig und die Tabelle rollt ungefähr ein Dutzend Pixel auf und ab, während die Zellenaktualisierungsanimation stattfindet.

Das Springen während der Größenänderung ist etwas nervig, aber mein Hauptproblem kommt danach. Wenn ich jetzt im Feed nach unten scrolle, ist das Scrollen wie zuvor reibungslos. Wenn ich jedoch über die Zelle nach oben scrolle, deren Größe ich gerade nach dem Hinzufügen von Kommentaren geändert habe, springt der Feed einige Male zurück, bevor er den oberen Rand des Feeds erreicht. Ich iOS8richte die Zellen für die automatische Größenanpassung für den Feed folgendermaßen ein:

// (FeedController.swift)
// tableView is the feed table containing PostCells
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 560

Wenn ich das entferne estimatedRowHeight, wird die Tabelle immer dann nach oben gescrollt, wenn sich die Zellenhöhe ändert. Ich fühle mich jetzt ziemlich festgefahren und könnte als neuer iOS-Entwickler alle Tipps gebrauchen, die Sie haben könnten.

Bryan Alger
quelle

Antworten:

120

Hier ist die beste Lösung, die ich gefunden habe, um diese Art von Problem zu lösen (Bildlaufproblem + reloadRows + iOS 8 UITableViewAutomaticDimension);

Es besteht darin, alle Höhen in einem Wörterbuch beizubehalten und sie (im Wörterbuch) zu aktualisieren, während die tableView die Zelle anzeigt.

Sie geben dann die gespeicherte Höhe in - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPathMethode zurück.

Sie sollten so etwas implementieren:

Ziel c

- (void)viewDidLoad {
    [super viewDidLoad];

    self.heightAtIndexPath = [NSMutableDictionary new];
    self.tableView.rowHeight = UITableViewAutomaticDimension;
}

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSNumber *height = [self.heightAtIndexPath objectForKey:indexPath];
    if(height) {
        return height.floatValue;
    } else {
        return UITableViewAutomaticDimension;
    }
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    NSNumber *height = @(cell.frame.size.height);
    [self.heightAtIndexPath setObject:height forKey:indexPath];
}

Swift 3

@IBOutlet var tableView : UITableView?
var heightAtIndexPath = NSMutableDictionary()

override func viewDidLoad() {
    super.viewDidLoad()

    tableView?.rowHeight = UITableViewAutomaticDimension
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    if let height = heightAtIndexPath.object(forKey: indexPath) as? NSNumber {
        return CGFloat(height.floatValue)
    } else {
        return UITableViewAutomaticDimension
    }
}

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    let height = NSNumber(value: Float(cell.frame.size.height))
    heightAtIndexPath.setObject(height, forKey: indexPath as NSCopying)
}
Dosdos
quelle
2
Wirklich einfache und effektive Lösung. Vielen Dank!
o15a3d4l11s2
1
Nur neugierig, gibt es einen Grund, warum ich das Swift Dictionary nicht anstelle von NSMutableDictionary verwenden könnte? Diese Lösung funktioniert übrigens super, danke!
AppreciateIt
3
@dosdos Gott segne dich, Alter!
Adnako
5
Für mich funktioniert es immer noch nicht, ich habe eine selbst definierte Bildansicht und basierend auf der Dimension der Breite und Höhe, um die Höhenbeschränkung zu aktualisieren. Selbst wenn die Zellenhöhen zwischengespeichert sind, springt das Scrollen immer noch, insbesondere bevor eine neue Zelle mit einer anderen Höhe als die aktuelle auf dem Bildschirm in den Bildschirm gescrollt wird.
TonyTony
1
Fabelhaft! Wunderbar! Es funktioniert wirklich gut !!!!!! Ich danke dir sehr! Ich denke, Sie können es als Lösung markieren.
ndominati2
30

Wir hatten das gleiche Problem. Dies beruht auf einer schlechten Schätzung der Zellenhöhe, die dazu führt, dass das SDK eine schlechte Höhe erzwingt, die beim Zurückscrollen zum Springen der Zellen führt. Je nachdem, wie Sie Ihre Zelle erstellt haben, können Sie dies am besten beheben, indem Sie die UITableViewDelegateMethode implementieren- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath

Solange Ihre Schätzung dem tatsächlichen Wert der Zellenhöhe ziemlich nahe kommt, wird das Springen und Ruckeln fast aufgehoben. So haben wir es implementiert, Sie erhalten die Logik:

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    // This method will get your cell identifier based on your data
    NSString *cellType = [self reuseIdentifierForIndexPath:indexPath];

    if ([cellType isEqualToString:kFirstCellIdentifier])
        return kFirstCellHeight;
    else if ([cellType isEqualToString:kSecondCellIdentifier])
        return kSecondCellHeight;
    else if ([cellType isEqualToString:kThirdCellIdentifier])
        return kThirdCellHeight;
    else {
        return UITableViewAutomaticDimension;
    }
}

Swift 2-Unterstützung hinzugefügt

func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    // This method will get your cell identifier based on your data
    let cellType = reuseIdentifierForIndexPath(indexPath)

    if cellType == kFirstCellIdentifier 
        return kFirstCellHeight
    else if cellType == kSecondCellIdentifier
        return kSecondCellHeight
    else if cellType == kThirdCellIdentifier
        return kThirdCellHeight
    else
        return UITableViewAutomaticDimension  
}
Gabriel Cartier
quelle
1
Am Ende habe ich die heightForRowAtIndexPath-Methode implementiert und das Ergebnis zwischengespeichert, um die Leistung zu verbessern, da es etwas kompliziert ist und der Feed lang sein kann. Wenn ein Benutzer Kommentare zu einem der Beiträge im Feed hinzufügt / löscht oder lädt, mache ich die Höhenberechnung für diese Zelle ungültig, damit sie während des Bildlaufs neu berechnet wird. Das Ruckeln ist weg, ich wünschte, ich könnte den Code vereinfachen und die neuen Funktionen zur Höhenberechnung nutzen, aber ich könnte es nicht gut genug mit meiner TableViewCell
Bryan Alger
2
Haben Sie es mit der oben beschriebenen Methode versucht? So sollte es mit iOS 8 gemacht werden, die Höhe sollte nicht berechnet werden, da sich das Framework bereits darum kümmert. Wenn Sie die heightForRowAtIndexPath-Methode implementieren, überschreiben Sie einfach das Verhalten des SDK.
Gabriel Cartier
7
@BryanAlger ist korrekt. Die automatischen Zeilenhöhen sind für Tabellen mit vielen Zeilen mit großen Höhenunterschieden einfach nicht verwendbar. Für ein zuverlässiges reibungsloses Scrollen müssen Sie in der heightForRowAtIndexPath-Methode korrekte Ergebnisse angeben, vorzugsweise mit zwischengespeicherten Zeilenhöhen. Andernfalls wird es ruckelig, wenn die tableView ihre contentSize aktualisieren muss, insbesondere in Fällen, in denen Sie einen anderen View-Controller drücken oder anzeigen und zurückkehren. Leider können automatische Zeilenhöhen nur für einfache Tabellenansichten mit wenigen Zeilen verwendet werden.
Jamie Hamick
2
Wenn Sie heightForRowAtIndexPath implementieren, verwenden Sie nicht die Leistung der automatischen Dimension der Zellenhöhe. Sicher, die Implementierung wird funktionieren, aber dann sind Ihre Zellen nicht dynamisch.
Gabriel Cartier
1
In meinem Fall hatte ich bereits die Zellenkonfiguration, die Höhenberechnung und das Höhen-Caching implementiert heightForRowAtIndexPath, hatte aber immer noch einen ruckartigen UITableViewBildlauf. Befolgen Sie die Antwort von @GabrielCartier und fügen Sie je nach Zelltyp eine spezifischere Logik hinzu, um das Problem wirklich zu lösen. Vielen Dank!
Sakiboy
23

dosdos Antwort hat bei mir in Swift 2 funktioniert

Erkläre den Ivar

var heightAtIndexPath = NSMutableDictionary()

in func viewDidLoad ()

func viewDidLoad() {
  .... your code
  self.tableView.rowHeight = UITableViewAutomaticDimension
}

Fügen Sie dann die folgenden 2 Methoden hinzu:

override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
   let height = self.heightAtIndexPath.objectForKey(indexPath)
   if ((height) != nil) {
     return CGFloat(height!.floatValue)
   } else {
    return UITableViewAutomaticDimension
   }
 }

override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
  let height = cell.frame.size.height
  self.heightAtIndexPath.setObject(height, forKey: indexPath)
}

SWIFT 3:

var heightAtIndexPath = [IndexPath: CGFloat]()

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

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    self.heightAtIndexPath[indexPath] = cell.frame.size.height
}
Ranknoodle
quelle
1
Vielen Dank! Funktioniert super :)
Michael
Fantastisches, entferntes Flimmern überhaupt.
AVEbrahimi
Update für SWIFT 3 hinzugefügt (vergessen Sie nicht die self.tableView.rowHeight = UITableViewAutomaticDimension in viewDidLoad)
MLBDG
Hat mir leider nicht wirklich geholfen. Auto-Layout ist hier etwas seltsam.
Nickdnk
1
Hey, ich habe deine Antwort bearbeitet, um ein getipptes Wörterbuch zu verwenden. Ich wollte nur meinen eigenen schnellen 4-Code als Antwort einfügen, fand aber, dass das Bearbeiten Ihres Codes ausreichen würde. Ich hoffe es macht dir nichts aus.
Manmal
3

@dosdos Lösung funktioniert gut

Aber es gibt etwas, das Sie hinzufügen sollten

folgende @dosdos Antwort

Swift 3/4

@IBOutlet var tableView : UITableView!
var heightAtIndexPath = NSMutableDictionary()

override func viewDidLoad() {
    super.viewDidLoad()

    tableView?.rowHeight = UITableViewAutomaticDimension
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    if let height = heightAtIndexPath.object(forKey: indexPath) as? NSNumber {
        return CGFloat(height.floatValue)
    } else {
        return UITableViewAutomaticDimension
    }
}

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    let height = NSNumber(value: Float(cell.frame.size.height))
    heightAtIndexPath.setObject(height, forKey: indexPath as NSCopying)
}

Verwenden Sie diese Zeilen dann, wann immer Sie möchten. Für mich verwende ich sie in textDidChange

  1. Laden Sie zuerst Tableview neu
  2. Update-Einschränkung
  3. Bewegen Sie sich schließlich nach oben

    tableView.reloadData()
    self.tableView.layoutIfNeeded()
    self.tableView.setContentOffset(CGPoint.zero, animated: true)
    
Basilikum
quelle
2

Ich hatte auch das gleiche Problem. Ich habe eine Problemumgehung gefunden, die den Ruck jedoch nicht vollständig behebt. Aber es scheint viel besser zu sein als beim vorherigen abgehackten Scrollen.

Versuchen Sie in Ihrer UITableViewDelegatenmethode :cellForRowAtIndexPath:, die Einschränkungen mithilfe der folgenden beiden Methoden zu aktualisieren, bevor Sie die Zelle zurückgeben. (Schnelle Sprache)

cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()

BEARBEITEN: Möglicherweise müssen Sie auch mit dem tableView.estimatedRowHeightWert herumspielen, um ein flüssigeres Scrollen zu erzielen.

Vishal Chandran
quelle
5
Ich würde die Verwendung dieser Methode nicht empfehlen. Das Aufrufen von Auto-Layout-Methoden in einer Methode wie cellForRowAtIndexPath kann die Leistung von TableView erheblich beeinträchtigen.
Gabriel Cartier
1

Folgende @dosdos Antwort.

Ich fand es auch interessant zu implementieren: tableView(tableView: didEndDisplayingCell: forRowAtIndexPath:

Speziell für meinen Code, bei dem sich die Zelle dynamisch ändert, während die Zelle bereits auf dem Bildschirm angezeigt wird. Das Aktualisieren des Wörterbuchs auf diese Weise hilft beim zweiten Anzeigen der Zelle.

var heightAtIndexPath = [NSIndexPath : NSNumber]()

....

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = UITableViewAutomaticDimension

....

extension TableViewViewController: UITableViewDelegate {

    //MARK: - UITableViewDelegate

    func tableView(tableView: UITableView,
                   estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {

        let height = heightAtIndexPath[indexPath]

        if let height = height {

            return CGFloat(height)
        }
        else {

            return UITableViewAutomaticDimension
        }
    }

    func tableView(tableView: UITableView,
                   willDisplayCell cell: UITableViewCell,
                                   forRowAtIndexPath indexPath: NSIndexPath) {

        let height: NSNumber = CGRectGetHeight(cell.frame)
        heightAtIndexPath[indexPath] = height
    }

    func tableView(tableView: UITableView,
                   didEndDisplayingCell cell: UITableViewCell,
                                        forRowAtIndexPath indexPath: NSIndexPath) {

        let height: NSNumber = CGRectGetHeight(cell.frame)
        heightAtIndexPath[indexPath] = height
    }
}
Gabriel.Massana
quelle