it-swarm.com.de

Jerky-Scrolling nach der Aktualisierung von UITableViewCell mit UITableViewAutomaticDimension

Ich baue eine App, die eine Feedansicht für vom Nutzer eingereichte Beiträge enthält. Diese Ansicht hat eine UITableView mit einer benutzerdefinierten UITableViewCell-Implementierung. In dieser Zelle habe ich eine weitere UITableView zum Anzeigen von Kommentaren. Der Gist ist ungefähr so:

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

Der anfängliche Feed wird mit 3 Kommentaren für die Vorschau heruntergeladen. Wenn jedoch weitere Kommentare vorhanden sind oder der Benutzer einen Kommentar hinzufügt oder löscht, möchte ich die PostCell in der Feedtabellenansicht aktualisieren, indem CommentCells zu den Kommentaren hinzugefügt oder entfernt wird Tabelle innerhalb der PostCell. Ich verwende 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
}

Damit ist das Update gut gelungen. Der Beitrag wird an Ort und Stelle aktualisiert, wobei Kommentare wie erwartet innerhalb der PostCell hinzugefügt oder entfernt werden. Ich benutze die automatische Größenanpassung PostCells in der Feed-Tabelle. Die Kommentartabelle von PostCell wird erweitert, um alle Kommentare anzuzeigen, aber die Animation ist etwas ruckelig und die Tabelle blättert ein Dutzend Pixel auf und ab, während die Zellaktualisierungsanimation stattfindet.

Das Springen während der Größenänderung ist etwas ärgerlich, aber mein Hauptproblem kommt danach. Wenn ich nun im Feed nach unten scrolle, ist das Scrollen wie zuvor glatt, aber wenn ich über die Zelle nach oben scrolle, die ich gerade nach dem Hinzufügen von Kommentaren geändert habe, springt der Feed ein paar Mal zurück, bevor er den oberen Rand des Feeds erreicht. Ich iOS8 automatische Größenanpassung von Zellen für den Feed wie folgt:

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

Wenn ich die Variable estimatedRowHeight entferne, wird die Tabelle bei jeder Änderung der Zellenhöhe nach oben verschoben. Ich fühle mich jetzt ziemlich fest damit beschäftigt und könnte als neuer iOS-Entwickler alle möglichen Tipps verwenden.

71
Bryan Alger

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

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

Die gespeicherte Höhe wird dann in der - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath-Methode zurückgegeben.

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)
}
109
dosdos

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ückblättern der Zellen zum Springen der Zellen führt. Je nachdem, wie Sie Ihre Zelle aufgebaut haben, können Sie dies am besten beheben, indem Sie die UITableViewDelegate-Methode - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath implementieren.

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

Unterstützung für Swift 2 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  
}
29
Gabriel Cartier

antwort von dosdos arbeitete für mich in Swift 2

Deklarieren Sie den Ivar

var heightAtIndexPath = NSMutableDictionary()

in func viewDidLoad ()

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

Fügen Sie dann die folgenden zwei 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
}
18
Ranknoodle

Ich stand auch vor dem gleichen Problem. Ich habe zwar eine Problemumgehung gefunden, aber der Ruck ist nicht vollständig behoben. Aber es scheint viel besser zu sein als beim vorherigen abgehackten Scrollen.

Verwenden Sie in Ihrer UITableView-Delegatenmethode :cellForRowAtIndexPath: die folgenden zwei Methoden, um die Einschränkungen zu aktualisieren, bevor Sie die Zelle zurückgeben. (Schnelle Sprache)

cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()

EDIT: Möglicherweise müssen Sie auch mit dem tableView.estimatedRowHeight-Wert herumspielen, um einen reibungsloseren Bildlauf zu erzielen.

2
Vishal Chandran

Folgende @dosdos Antwort.

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

Speziell für meinen Code, bei dem die Zelle Einschränkungen 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
    }
}
1
Gabriel.Massana

@dosdos-Lösung funktioniert einwandfrei 

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

dann diese Zeilen verwenden, wann immer Sie wollen, für mich verwende ich es in textDidChange

  1. laden Sie zuerst Tableview neu 
  2. aktualisierungsbedingung
  3. endlich zum oberen Tableview wechseln

    tableView.reloadData()
    self.tableView.layoutIfNeeded()
    self.tableView.setContentOffset(CGPoint.zero, animated: true)
    
0
Basil