ReloadData() UITableView с динамическими высотами ячеек вызывает скачкообразную прокрутку
Я чувствую, что это может быть общей проблемой, и мне было интересно, есть ли какое-то общее решение для этого.
По сути, мой UITableView имеет динамическую высоту ячейки для каждой ячейки. Если я не на вершине UITableView и я tableView.reloadData()
, прокрутка вверх становится нервной.
Я полагаю, что это связано с тем фактом, что, поскольку я перезагружал данные при прокрутке вверх, UITableView пересчитывает высоту для каждой ячейки, входящей в видимость. Как мне смягчить это, или как я могу только перезагрузить данные из определенного IndexPath до конца UITableView?
Кроме того, когда мне удается прокрутить до самого верха, я могу прокрутить назад вниз, а затем вверх, без проблем без прыжков. Это наиболее вероятно, потому что высоты UITableViewCell уже были рассчитаны.
Ответы
Ответ 1
Чтобы предотвратить прыжки, вы должны сохранять высоты ячеек при их загрузке и давать точное значение в tableView:estimatedHeightForRowAtIndexPath
:
// declare cellHeightsDictionary
NSMutableDictionary *cellHeightsDictionary;
// initialize in code (thanks to @Gerharbo)
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;
}
Ответ 2
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
}
Ответ 3
Прыжок из-за плохой оценки высоты. Чем больше предполагаемый 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
}
Ответ 4
Ответ @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 UITableViewAutomaticDimension
}
Ответ 5
Я перепробовал все обходные пути выше, но ничего не получалось.
Потратив часы и пройдя через все возможные разочарования, придумал, как это исправить. Это решение спасает жизнь! Работал как шарм!
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()
ИЛИ вы можете добавить эти строки в свой метод awakeFromNib()
UITableViewCell
layer.shouldRasterize = true
layer.rasterizationScale = UIScreen.main.scale
и сделать нормальный reloadData()
Ответ 6
Я столкнулся с этим сегодня и заметил:
- Это только iOS 8.
- Переопределение
cellForRowAtIndexPath
не помогает.
Исправление было довольно простым:
Заменить estimatedHeightForRowAtIndexPath
и убедиться, что он вернет правильные значения.
С этим все странные дрожания и прыжки в моем UITableViews прекратились.
ПРИМЕЧАНИЕ. Я действительно знаю размер моих ячеек. Возможны только два возможных значения. Если ваши ячейки действительно имеют переменный размер, вы можете захотеть кэшировать cell.bounds.size.height
из tableView:willDisplayCell:forRowAtIndexPath:
Ответ 7
Фактически вы можете перезагрузить только определенные строки, используя reloadRowsAtIndexPaths
, ex:
tableView.reloadRowsAtIndexPaths(indexPathArray, withRowAnimation: UITableViewRowAnimation.None)
Но, в общем, вы также можете анимировать изменения высоты таблицы таблицы так:
tableView.beginUpdates()
tableView.endUpdates()
Ответ 8
Я использую больше способов, как это исправить:
Для контроллера просмотра:
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
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)
Ответ 9
Вот немного более короткая версия:
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
return self.cellHeightsDictionary[indexPath] ?? UITableViewAutomaticDimension
}
Ответ 10
Переопределение методаmetheightForRowAtIndexPath с высоким значением, например, 300f
Это должно решить проблему :)
Ответ 11
Этот работал для меня в Swift4:
extension UITableView {
func reloadWithoutAnimation() {
let lastScrollOffset = contentOffset
reloadData()
layoutIfNeeded()
setContentOffset(lastScrollOffset, animated: false)
}
}
Ответ 12
Есть ошибка, которая, как мне кажется, была введена в iOS11.
То есть при reload
tableView 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)
Этот ответ был получен отсюда
Ответ 13
У меня есть 2 разных высоты клетки.
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
return Helper.makeDeviceSpecificCommonSize(cellHeight)
}
После того как я добавил оценочный HeightForRowAt, прыжков больше не было.
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
let cellHeight = CGFloat(checkIsCleanResultSection(index: indexPath.row) ? 130 : 160)
return Helper.makeDeviceSpecificCommonSize(cellHeight)
}
Ответ 14
Попробуйте вызвать cell.layoutSubviews()
перед возвратом ячейки в func cellForRowAtIndexPath(_ indexPath: NSIndexPath) -> UITableViewCell?
. Это известная ошибка в iOS8.
Ответ 15
Ни одно из этих решений не помогло мне. Вот что я сделал со 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
}
}
Теперь дайте приблизительную высоту строки для каждой ячейки в tableViewtimateHeightForRowAt. Будьте точны, как можете...
func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
if indexPath.row == 0 {
return 400 // or whatever YourTableViewCell height is
} else if indexPath.row == 1 {
return 231 // or whatever YourSecondTableViewCell height is
} else {
return 216 // or whatever YourThirdTableViewCell height is
}
}
Это должно работать...
Мне не нужно было сохранять и устанавливать contentOffset при вызове tableView.reloadData()
Ответ 16
Вы можете использовать следующее в 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
Ответ 17
У меня было такое поведение при прыжке, и я изначально смог смягчить его, установив точную приблизительную высоту заголовка (потому что у меня было только 1 возможное представление заголовка), однако затем прыжки начали происходить именно внутри заголовков, больше не влияя на всю таблицу.
После ответов здесь я понял, что это связано с анимацией, поэтому я обнаружил, что табличное представление находится внутри стекового представления, и иногда мы вызываем stackView.layoutIfNeeded()
внутри блока анимации. Мое окончательное решение состояло в том, чтобы этот вызов не происходил, если только "действительно" не нужно, потому что макет "при необходимости" имел визуальное поведение в этом контексте, даже когда "не нужно".