Прокрутка Jerky после обновления UITableViewCell с помощью UITableViewAutomaticDimension

Я создаю приложение, которое имеет представление фида для отправленных пользователем сообщений. Это представление имеет UITableView с пользовательской реализацией UITableViewCell. Внутри этой ячейки у меня есть еще один UITableView для отображения комментариев. Суть такова:

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

Первоначальный канал будет загружен с 3 комментариями для предварительного просмотра, но если есть больше комментариев или если пользователь добавляет или удаляет комментарий, я хочу обновить PostCell на месте внутри представления таблицы фидов, добавив или удалив CommentCells в таблицу комментариев внутри PostCell. В настоящее время я использую следующий помощник, чтобы выполнить это:

// (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 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
}

Это прекрасно подходит для обновления. Сообщение обновляется на месте с комментариями, добавленными или удаленными внутри PostCell, как ожидалось. Я использую автоматическое определение размера PostCells в таблице подачи. Таблица комментариев PostCell расширяется, чтобы отображать все комментарии, но анимация немного отрывистая, а таблица сортирует прокрутки вверх и вниз на дюжину пикселей или около того, пока происходит анимация обновления ячейки.

Перескакивание при изменении размера немного раздражает, но моя основная проблема возникает позже. Теперь, если я прокручу вниз в фиде, прокрутка будет гладкой, как раньше, но если я прокручу вверх над ячейкой, я просто изменил размер после добавления комментариев, фид несколько раз скачет назад, прежде чем он достигнет вершины фида. Я настраиваю iOS8 автоматические ячейки прорисовки для Feed следующим образом:

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

Если я удалю estimatedRowHeight, таблица просто прокручивается в начало в любое время, когда изменяется высота ячейки. Я чувствую себя довольно застрявшим на этом сейчас и как новый разработчик iOS, мог бы использовать любые советы, которые у вас могут быть.

Ответ 1

Вот лучшее решение, которое я нашел для решения этой проблемы (проблема прокрутки + reloadRows + iOS 8 UITableViewAutomaticDimension);

Он состоит из сохранения каждой высоты словаря и обновления (в словаре), поскольку tableView отображает ячейку.

Затем вы вернете сохраненную высоту в методе - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath.

Вы должны реализовать что-то вроде этого:

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

Ответ 2

У нас была та же проблема. Это происходит из-за плохой оценки высоты ячейки, которая заставляет SDK заставлять плохую высоту, которая вызовет скачок клеток при прокрутке назад. В зависимости от того, как вы построили свою ячейку, лучший способ исправить это - реализовать метод UITableViewDelegate - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath

Пока ваша оценка довольно близка к реальной стоимости высоты ячейки, это почти отменяет прыжки и рывок. Здесь, как мы его реализовали, вы получите логику:

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

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  
}

Ответ 3

dosdos ответ работал у меня в Swift 2

Объявить ivar

var heightAtIndexPath = NSMutableDictionary()

в func viewDidLoad()

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

Затем добавьте следующие 2 метода:

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 = NSMutableDictionary()

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

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

Ответ 4

У меня тоже была проблема. Я нашел обходное решение, но он не полностью исправил рывок. Но это кажется намного лучше, чем предыдущая изменчивая прокрутка.

В методе делегата UITableView :cellForRowAtIndexPath: попробуйте использовать следующие два метода для обновления ограничений перед возвратом ячейки. (Язык Swift)

cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()

РЕДАКТИРОВАТЬ: Вам может понадобиться играть со значением tableView.estimatedRowHeight, чтобы получить более плавную прокрутку.

Ответ 5

После ответа @dosdos.

Я также нашел интересным реализовать: tableView(tableView: didEndDisplayingCell: forRowAtIndexPath:

Специально для моего кода, где ячейка динамически меняет ограничения, пока ячейка уже отображается на экране. Обновление такого словаря помогает во второй раз отображать ячейку.

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

Ответ 6

Решение

@dosdos работает нормально

но есть что-то, что вы должны добавить

после ответа @dosdos

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

затем используйте эти строки, когда захотите, для меня я использую его внутри textDidChange

  • первая перезагрузка Tableview
  • ограничение обновления
  • наконец перейдите в начало Tableview

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