reloadData () из UITableView с динамической высотой ячеек вызывает резкую прокрутку


148

Я чувствую, что это может быть обычная проблема, и мне было интересно, есть ли какое-то общее решение.

По сути, мой UITableView имеет динамическую высоту ячеек для каждой ячейки. Если я не нахожусь в верхней части UITableView и я tableView.reloadData(), прокрутка вверх становится прерывистой.

Я считаю, что это связано с тем, что, поскольку я перезагружал данные при прокрутке вверх, UITableView пересчитывает высоту для каждой ячейки, становящейся видимой. Как мне смягчить это или как мне перезагрузить данные только из определенного IndexPath в конец UITableView?

Кроме того, когда мне удается прокрутить до самого верха, я могу прокрутить назад вниз, а затем вверх, никаких проблем с прыжками. Скорее всего, это связано с тем, что высоты UITableViewCell уже были рассчитаны.


Пара вещей ... (1) Да, вы определенно можете перезагрузить определенные строки, используя reloadRowsAtIndexPaths. Но (2) что вы подразумеваете под «нервным» и (3) вы установили примерную высоту строки? (Просто пытаюсь выяснить, есть ли лучшее решение, которое позволило бы вам динамически обновлять таблицу.)
— Линдси Скотт

@LyndseyScott, да, я установил приблизительную высоту строки. Под «нервным» я подразумеваю, что при прокрутке вверх строки смещаются вверх. Я считаю, что это связано с тем, что я установил предполагаемую высоту строки 128, а затем, когда я прокручиваю вверх, все мои сообщения выше в UITableView меньше, поэтому он сжимает высоту, заставляя мою таблицу подпрыгивать. Я думаю о том, чтобы сделать reloadRowsAtIndexPaths от строки xдо последней строки в моем TableView ... но поскольку я вставляю новые строки, это не сработает, я не могу знать, каким будет конец моего tableview, прежде чем я перезагружу данные.
— Дэвид

2
@LyndseyScott все еще не могу решить проблему, есть ли хорошее решение?
— рад

1
Вы когда-нибудь находили решение этой проблемы? Я столкнулся с той же проблемой, что и в вашем видео.
— user3344977

1
Ни один из приведенных ниже ответов не помог мне.
— Srujan Simha

Ответы:


234

Чтобы предотвратить скачки, вы должны сохранять высоту ячеек при их загрузке и указывать точное значение в tableView:estimatedHeightForRowAtIndexPath:

Swift:

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] ?? UITableView.automaticDimension
}

Цель C:

// declare cellHeightsDictionary
NSMutableDictionary *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;
}

1
Спасибо, ты действительно спас мне день :) Работает и в objc
— Артем З.

3
Не забудьте инициализировать cellHeightsDictionary: cellHeightsDictionary = [NSMutableDictionary dictionary];
— Gerharbo

1
estimatedHeightForRowAtIndexPath:возвращает двойное значение, может вызвать *** Assertion failure in -[UISectionRowData refreshWithSection:tableView:tableViewRowData:]ошибку. return floorf(height.floatValue);Вместо этого, чтобы исправить это .
— liushuaikobe

Привет, @lgor, у меня такая же проблема, и я пытаюсь реализовать ваше решение. Проблема, которую я получаю, - это оценкаHeightForRowAtIndexPath вызывается до willDisplayCell, поэтому высота ячейки не вычисляется при вызове EstimatedHeightForRowAtIndexPath. Любая помощь?
— Мадхури

1
Эффективные высоты @Madhuri должны вычисляться в «heightForRowAtIndexPath», который вызывается для каждой ячейки на экране непосредственно перед willDisplayCell, который устанавливает высоту в словаре для последующего использования в EstimatedRowHeight (при перезагрузке таблицы).
— Donnit

110

Swift 3 версия принятого ответа.

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 
}

Спасибо, это отлично сработало! на самом деле я смог удалить свою реализацию func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {, она обрабатывает все необходимые мне вычисления высоты.
— Наталья

После многих часов борьбы с упорными прыжками я понял, что забыл добавить UITableViewDelegateв свой класс. Соответствие этому протоколу необходимо, поскольку он содержит указанную выше willDisplayфункцию. Я надеюсь, что смогу спасти кого-то от такой же борьбы.
— MJQZ1347

Спасибо за быстрый ответ. В моем случае у меня было какое-то СУПЕР странное поведение ячеек, выходящих из строя при перезагрузке, когда представление таблицы было прокручено до / около дна. С этого момента я буду использовать это всякий раз, когда у меня будут саморазмерные ячейки.
— Trev14

Отлично работает в Swift 4.2
— Адам С.

Спасатель жизни. Так полезно при попытке добавить больше элементов в источник данных. Предотвращает прыжок только что добавленных ячеек в центр экрана.
— Филип Борбон

41

Прыжок вызван неверной оценкой высоты. Чем больше оценкаRowHeight отличается от фактической высоты, тем больше таблица может подскочить при перезагрузке, особенно чем ниже она была прокручена. Это связано с тем, что предполагаемый размер таблицы радикально отличается от ее фактического размера, что вынуждает таблицу корректировать размер содержимого и смещение. Таким образом, расчетная высота не должна быть случайной величиной, а должна быть близкой к тому, что, по вашему мнению, будет высотой. Я также испытал, когда я установил, UITableViewAutomaticDimension если ваши ячейки одного типа, тогда

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

если у вас есть множество ячеек в разных секциях, то я думаю, что лучшее место

func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
     //return different sizes for different cells if you need to
     return 100
}

2
спасибо, именно поэтому мой tableView был таким нервным.
— Луи де Декер

1
Старый ответ, но он по-прежнему актуален по состоянию на 2018 год. В отличие от всех других ответов, этот предлагает один раз установить EstimatedRowHeigh в viewDidLoad, что помогает, когда ячейки имеют одинаковую или очень похожую высоту. Спасибо. Кстати, в качестве альтернативы esimatedRowHeight можно установить через Interface Builder в Size Inspector> Table View> Estimate.
— Виталий

при условии, что более точная оценка высоты мне помогла. У меня также был стиль представления многосекционной сгруппированной таблицы, и я должен был реализовать егоtableView(_:estimatedHeightForHeaderInSection:)
— nteissler

25

Ответ @Igor в этом случае работает нормально, егоSwift-4код.

// declaration & initialization  
var cellHeightsDictionary: [IndexPath: CGFloat] = [:]  

в следующих методах 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 UITableView.automaticDimension
}

6
Как справиться с вставкой / удалением строк с помощью этого решения? TableView скачет, так как данные словаря не актуальны.
— Алексей Чеканов

1
работает отлично! особенно в последней ячейке при перезагрузке строки.
— Ning

20

Я пробовал все описанные выше обходные пути, но ничего не помогло.

Проведя часы и пережив все возможные разочарования, нашел способ исправить это. Это решение - спаситель жизни! Работал как шарм!

Swift 4

let lastContentOffset = tableView.contentOffset
tableView.beginUpdates()
tableView.endUpdates()
tableView.layer.removeAllAnimations()
tableView.setContentOffset(lastContentOffset, animated: false)

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

extension UITableView {

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

наконец-то ..

tableView.reloadWithoutAnimation()

ИЛИ вы можете добавить эту строку в свой UITableViewCell awakeFromNib()метод

layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale

и делай нормально reloadData()


1
Как это делает перезагрузку? Вы называете это, reloadWithoutAnimationно где reloadчасть?
— Мэтт

@matt, вы можете tableView.reloadData()сначала позвонить, а затем tableView.reloadWithoutAnimation()он все еще работает.
— Srujan Simha

Большой! Ничего из вышеперечисленного у меня тоже не сработало. Даже высота и расчетная высота полностью совпадают. Интересно.
— TY Kucuk

1
Не работай на меня. Это сбой в tableView.endUpdates (). Кто-нибудь может мне помочь!
— Какаши

12

Я использую больше способов как это исправить:

Для контроллера представления:

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 
}

как расширение для UITableView

extension UITableView {
  func reloadSectionWithouAnimation(section: Int) {
      UIView.performWithoutAnimation {
          let offset = self.contentOffset
          self.reloadSections(IndexSet(integer: section), with: .none)
          self.contentOffset = offset
      }
  }
}

Результат

tableView.reloadSectionWithouAnimation(section: indexPath.section)

1
Ключевым моментом для меня была реализация здесь его расширения UITableView. Очень умный. Спасибо rastislv
— BennyTheNerd

Работает отлично, но имеет только один недостаток: вы теряете анимацию при вставке верхнего, нижнего колонтитула или строки.
— Суфиан Хоссам

Где будет вызываться reloadSectionWithouAnimation? Так, например, пользователи могут размещать изображение в моем приложении (например, в Instagram); Я могу изменить размер изображений, но в большинстве случаев мне нужно прокрутить ячейку таблицы за пределы осыпи, чтобы это произошло. Я хочу, чтобы ячейка имела правильный размер после того, как таблица прошла через reloadData.
— Люк Ирвин

11

Я столкнулся с этим сегодня и заметил:

  1. Действительно, это только iOS 8.
  2. Переопределение cellForRowAtIndexPathне помогает.

Исправить на самом деле было довольно просто:

Переопределите estimatedHeightForRowAtIndexPathи убедитесь, что он возвращает правильные значения.

С этим все странное дрожание и скачки в моих UITableViews прекратились.

ПРИМЕЧАНИЕ: я действительно знаю размер своих клеток. Есть только два возможных значения. Если ваши ячейки действительно имеют переменный размер, то вы можете захотеть кэшировать cell.bounds.size.heightизtableView:willDisplayCell:forRowAtIndexPath:


2
Исправлено переопределение метода EstimatedHeightForRowAtIndexPath с высоким значением, например 300f
— Flappy

1
@Flappy интересно, как работает предоставленное вами решение, и оно короче других предлагаемых методов. Рассмотрите возможность публикации этого сообщения в качестве ответа.
— Рохан Санап,

9

Фактически вы можете перезагрузить только определенные строки, используя reloadRowsAtIndexPaths, например:

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

Но, как правило, вы также можете анимировать изменение высоты ячейки таблицы следующим образом:

tableView.beginUpdates()
tableView.endUpdates()

Я пробовал использовать метод beginUpdates / endUpdates, но он влияет только на видимые строки моей таблицы. У меня все еще проблема при прокрутке вверх.
— Дэвид

@David Вероятно, потому что вы используете приблизительную высоту строк.
— Линдси Скотт,

Следует ли мне избавиться от своих EstimatedRowHeights и заменить их beginUpdates и endUpdates?
— Дэвид

@David Вы бы ничего не «заменяли», но это действительно зависит от желаемого поведения ... Если вы хотите использовать предполагаемую высоту строк и просто перезагрузить индексы ниже текущей видимой части таблицы, вы можете сделать это как Я сказал, используя reloadRowsAtIndexPaths
— Линдси Скотт

Одна из моих проблем с использованием метода reladRowsAtIndexPaths заключается в том, что я реализую бесконечную прокрутку, поэтому, когда я перезагружаю данные, это происходит потому, что я просто добавил еще 15 строк в источник данных. Это означает, что indexPaths для этих строк еще не существует в UITableView
— Дэвид,

3

Вот немного более короткая версия:

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

3

Переопределение метода EstimatedHeightForRowAtIndexPath высоким значением, например 300f

Это должно решить проблему :)


2

Есть ошибка, которая, как мне кажется, появилась в iOS11.

Это когда вы делаете reloadtableView contentOffSetнеожиданно изменяется. Фактически contentOffsetне должно измениться после перезагрузки. Обычно это происходит из-за просчетовUITableViewAutomaticDimension

Вы должны сохранить contentOffSetего и вернуть его к сохраненному значению после завершения перезагрузки.

func reloadTableOnMain(with offset: CGPoint = CGPoint.zero){

    DispatchQueue.main.async { [weak self] () in

        self?.tableView.reloadData()
        self?.tableView.layoutIfNeeded()
        self?.tableView.contentOffset = offset
    }
}

Как вы это используете?

someFunctionThatMakesChangesToYourDatasource()
let offset = tableview.contentOffset
reloadTableOnMain(with: offset)

Этот ответ был получен отсюда


2

Этот работал у меня в Swift4:

extension UITableView {

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

1

Ни одно из этих решений не помогло мне. Вот что я сделал с Swift 4 и Xcode 10.1 ...

В viewDidLoad () объявите динамическую высоту строки таблицы и создайте правильные ограничения в ячейках ...

tableView.rowHeight = UITableView.automaticDimension

Также в viewDidLoad () зарегистрируйте все перья ячеек tableView в 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")

В tableView heightForRowAt возвращает высоту, равную высоте каждой ячейки в indexPath.row ...

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
    } 

}

Теперь укажите предполагаемую высоту строки для каждой ячейки в tableView EstimatedHeightForRowAt. Будьте точны, как можете ...

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
    } 

}

Это должно сработать ...

Мне не нужно было сохранять и устанавливать contentOffset при вызове tableView.reloadData ()


1

У меня 2 разной высоты ячейки.

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
        return Helper.makeDeviceSpecificCommonSize(cellHeight)
    }

После того, как я добавил EstimatedHeightForRowAt , прыжков больше не было.

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
    return Helper.makeDeviceSpecificCommonSize(cellHeight)
}

1

Один из подходов к решению этой проблемы, который я нашел, -

CATransaction.begin()
UIView.setAnimationsEnabled(false)
CATransaction.setCompletionBlock {
   UIView.setAnimationsEnabled(true)
}
tableView.reloadSections([indexPath.section], with: .none)
CATransaction.commit()

0

Попытайтесь позвонить, cell.layoutSubviews()прежде чем возвращать камеру func cellForRowAtIndexPath(_ indexPath: NSIndexPath) -> UITableViewCell?. Известная ошибка в iOS8.


0

Вы можете использовать следующее в ViewDidLoad()

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

У меня было такое поведение прыжков, и я изначально смог смягчить его, установив точную предполагаемую высоту заголовка (потому что у меня был только один возможный вид заголовка), однако затем прыжки начали происходить внутри заголовков, не затрагивая больше всю таблицу.

Следуя приведенным здесь ответам, я понял, что это связано с анимацией, поэтому я обнаружил, что представление таблицы находится внутри представления стека, и иногда мы вызывали stackView.layoutIfNeeded()внутри блока анимации. Мое последнее решение заключалось в том, чтобы убедиться, что этот вызов не произойдет, если «действительно» не нужно, потому что макет «при необходимости» имеет визуальное поведение в этом контексте, даже когда «не нужен».


0

Я была такая же проблема. У меня была разбивка на страницы и перезагрузка данных без анимации, но это не помогло прокрутке предотвратить прыжок. У меня разные размеры айфонов, на iphone8 прокрутка не шла, а на iphone7 + -

Я применил следующие изменения к функции viewDidLoad :

    self.myTableView.estimatedRowHeight = 0.0
    self.myTableView.estimatedSectionFooterHeight = 0
    self.myTableView.estimatedSectionHeaderHeight = 0

и моя проблема решена. Надеюсь, тебе это тоже поможет.


-1

На самом деле я обнаружил, если вы используете reloadRowsвызывающую проблему с прыжком. Тогда вы должны попробовать использовать reloadSectionsвот так:

UIView.performWithoutAnimation {
    tableView.reloadSections(NSIndexSet(index: indexPath.section) as IndexSet, with: .none)
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.