Уривчаста прокрутка після оновлення UITableViewCell на місці за допомогою UITableViewAutomaticDimension


78

Я створюю додаток, який має подання стрічки повідомлень, надісланих користувачами. Цей вигляд має 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'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
}

Це робить оновлення чудово. Публікація оновлюється на місці, додаючи або видаляючи коментарі всередині, PostCellяк очікувалося Я використовую автоматичний розмір PostCellsу таблиці подачі. Таблиця коментарів PostCellрозгортається, щоб показати всі коментарі, але анімація дещо уривчаста, і таблиця начебто прокручується вгору і вниз з десяток пікселів або близько того, поки відбувається анімація оновлень комірок.

Стрибки під час зміни розміру трохи дратують, але моя головна проблема виникає згодом. Тепер, якщо я прокручую вниз у стрічці, прокрутка стає плавною, як і раніше, але якщо я прокручую вище клітинку, яку я щойно змінив після додавання коментарів, стрічка кілька разів перескакує назад, перш ніж досягне верхньої частини стрічки. Я налаштовуюiOS8 автоматичні розміри комірок для каналу так:

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

Якщо я видаляю estimatedRowHeight, таблиця просто прокручується до початку в будь-який час, коли змінюється висота комірки. Я відчуваю себе досить застрягли в цьому зараз, і як новий розробник iOS, я міг би скористатися будь-якими порадами, які ви можете мати.

Відповіді:


120

Ось найкраще рішення, яке я знайшов для вирішення такого роду проблем (проблема прокрутки + reloadRows + iOS 8 UITableViewAutomaticDimension);

Він складається із збереження всіх висот у словнику та оновлення їх (у словнику), оскільки tableView відображатиме комірку.

Потім ви повернете збережену висоту в - (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPathметоді.

Ви повинні реалізувати щось подібне:

Завдання-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];
}

Стрімкий 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
Дійсно просте та ефективне рішення. Дякую!
o15a3d4l11s2

1
Просто цікаво, чи є якась причина, чому я не міг користуватися Swift Dictionary замість NSMutableDictionary? Це рішення, до речі, чудово працює, дякую!
Подякуйте

3
@dosdos Бог тебе благословить, чувак!
adnako

5
Для мене це все ще не працює, у мене є самовизначений imageView, і на основі розміру ширини та висоти оновити обмеження висоти. Навіть із кешованою висотою комірки, прокрутка все одно стрибає, особливо перед тим, як нова комірка з іншою висотою, ніж поточна на екрані, прокручується на екран.
TonyTony

1
Казково! Чудово! Це працює дуже добре !!!!!! Дуже дякую! Я думаю, ви можете позначити це як рішення.
ndominati2

30

У нас була та сама проблема. Це походить від поганої оцінки висоти комірки, що змушує 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  
}

1
У підсумку я застосував метод heightForRowAtIndexPath і кешував результат для підвищення продуктивності, оскільки він трохи задіяний, і стрічка може бути довгою. Коли користувач додає / видаляє або завантажує коментарі до одного з дописів у стрічці, я роблю недійсним обчислення висоти для цієї комірки, щоб воно було перераховано під час прокрутки. Дерзкості немає, я хотів би спростити код і використати нові функції обчислення висоти, але я не зміг змусити його працювати належним чином з моїм TableViewCell
Bryan Alger

2
Ви пробували з методом, який я описав вище? Це слід робити з iOS 8, висота не повинна обчислюватися, оскільки фреймворк вже піклується про це. Якщо ви реалізуєте метод heightForRowAtIndexPath, ви просто переоцінюєте поведінку SDK.
Габріель Картьє

7
@BryanAlger правильний. Автоматична висота рядків просто не придатна для таблиць з великою кількістю рядків із великою різницею висоти. Для надійної плавної прокрутки потрібно дати правильні результати за методом heightForRowAtIndexPath, бажано з кешованою висотою рядків. В іншому випадку ви отримаєте ривок, коли tableView потрібно оновити його contentSize, особливо у випадках, коли ви натискаєте або показуєте інший контролер подання та повертаєтесь. На жаль, автоматичні висоти рядків використовуються лише для простих табличних переглядів з кількома рядками.
Jamie Hamick

2
У випадку, коли ви реалізуєте heightForRowAtIndexPath, ви не використовуєте потужність автоматичного вимірювання висоти комірки. Звичайно, реалізація буде працювати, але тоді ваші клітини не динамічні.
Габріель Картьє

1
Для мого випадку я вже реалізував конфігурацію комірок, обчислення висоти та кешування висоти heightForRowAtIndexPath, але все ще мав UITableViewпрокручуваний прокрутку. Після відповіді @GabrielCartier та додавання більш конкретної логіки залежно від типу комірки дійсно допомогло та вирішило проблему, дякую!
Сакібой

23

відповідь dosdos спрацювала для мене у Swift 2

Заявіть івар

var heightAtIndexPath = NSMutableDictionary()

у функції 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 = [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
}

1
Дякую! Чудово працює :)
Майкл

Чудово, прибране мерехтіння взагалі.
AVEbrahimi

Додано оновлення для SWIFT 3 (не забувайте self.tableView.rowHeight = UITableViewAutomaticDimension у viewDidLoad)
MLBDG,

На жаль, мені це не допомогло. Авторозкладка тут трохи дивна.
nickdnk

1
привіт, я відредагував вашу відповідь, щоб скористатися набраним словником. я просто хотів вставити свій власний швидкий код 4 як відповідь, але виявив, що редагування вашого буде достатньо. сподіваюся, ви не проти.
Манмаль

3

Рішення @dosdos працює нормально

але є щось, що ви повинні додати

наступна відповідь @dosdos

Стрім 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

  1. спочатку перезавантажте
  2. обмеження на оновлення
  3. нарешті перейти на початок таблиці

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

2

Я теж стикався з тією ж проблемою. Я знайшов обхідний шлях, але це не повністю виправляє ривок. Але це, здається, набагато краще у порівнянні з попередньою непостійною прокруткою.

У UITableViewметоді делегата :cellForRowAtIndexPath:спробуйте використати наступні два методи для оновлення обмежень перед поверненням комірки. (Швидка мова)

cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()

РЕДАГУВАТИ: Можливо, вам також доведеться пограти зі tableView.estimatedRowHeightзначенням, щоб отримати більш плавну прокрутку.


5
Я не рекомендував би використовувати цей метод, оскільки виклик методів автоматичного макетування в методі, такому cellForRowAtIndexPath, може суттєво вплинути на продуктивність TableView.
Габріель Картьє

1

Після відповіді @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
    }
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.