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)
rastislv
источник
1
Ключевым моментом для меня была реализация здесь его расширения UITableView. Очень умный. Спасибо rastislv
BennyTheNerd
Работает отлично, но имеет только один недостаток: вы теряете анимацию при вставке верхнего, нижнего колонтитула или строки.
Суфиан Хоссам
Где будет вызываться reloadSectionWithouAnimation? Так, например, пользователи могут размещать изображение в моем приложении (например, в Instagram); Я могу изменить размер изображений, но в большинстве случаев мне нужно прокрутить ячейку таблицы за пределы осыпи, чтобы это произошло. Я хочу, чтобы ячейка имела правильный размер после того, как таблица прошла через reloadData.
Люк Ирвин
11

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

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

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

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

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

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

MarcWan
источник
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
}
jake1981
источник
3

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

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

Flappy
источник
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.

CrimeZone
источник
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)
}
Майкл
источник