Подтвердить что ты не робот

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

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

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

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

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

4b9b3361

Ответ 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() внутри блока анимации. Мое окончательное решение состояло в том, чтобы этот вызов не происходил, если только "действительно" не нужно, потому что макет "при необходимости" имел визуальное поведение в этом контексте, даже когда "не нужно".