reloadData () UITableView з динамічними висотами комірок викликає стрибкову прокрутку


142

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

В основному, мій UITableView має динамічну висоту комірок для кожної комірки. Якщо я не перебуваю на вершині UITableView, і я tableView.reloadData(), прокручування вгору стає стрибковим.

Я вважаю, що це пов'язано з тим, що, оскільки я перезавантажував дані, прокручуючи вгору, UITableView перераховує висоту для кожної комірки, що надходить у видимість. Як я пом'якшую це чи як я лише перезавантажую дані з певного IndexPath до кінця UITableView?

Далі, коли мені вдається прокрутити весь шлях до вершини, я можу прокручуватися назад вниз, а потім вгору, без проблем із стрибками. Це найімовірніше, тому що висоти UITableViewCell були вже обчислені.


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

@LyndseyScott, так, я встановив орієнтовну висоту рядка. Під стрибком я маю на увазі, що під час прокрутки вгору рядки зміщуються вгору. Я вважаю, це тому, що я встановив орієнтовну висоту рядка 128, а потім, коли я прокручую вгору, всі мої публікації вище в UITableView менші, тому вона скорочується по висоті, внаслідок чого моя таблиця стрибає. Я думаю про те, щоб зробити reloadRowsAtIndexPaths з рядка xдо останнього рядка в моєму TableView ... але, оскільки я вставляю нові рядки, це не спрацює, я не можу знати, яким буде кінець мого перегляду таблиці, перш ніж я перезавантажуюсь дані.
Девід

2
@LyndseyScott все ще не можу вирішити проблему, чи є хороше рішення?
рад

1
Ви коли-небудь знаходили рішення для цієї проблеми? У мене виникають точно такі ж проблеми, як і у вашому відео.
user3344977

1
Жодна з наведених відповідей не працювала для мене.
Srujan Simha

Відповіді:


221

Щоб запобігти стрибкам, слід зберегти висоту комірок, коли вони завантажуються, і вказати точне значення у tableView:estimatedHeightForRowAtIndexPath:

Швидкий:

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];
Герхарбо

1
estimatedHeightForRowAtIndexPath:повертає подвійне значення може спричинити *** Assertion failure in -[UISectionRowData refreshWithSection:tableView:tableViewRowData:]помилку. Щоб виправити це, return floorf(height.floatValue);натомість.
liushuaikobe

Привіт @lgor, у мене те саме питання, і я намагаюся реалізувати ваше рішення. Проблема, яку я отримую, оцінюєтьсяHeightForRowAtIndexPath, викликається перед willDisplayCell, тому висота комірки не обчислюється при виклику оцінюваного HeeForForvoAtIndexPath. Будь-яка допомога?
Мадхурі

1
Ефективні висоти @Madhuri повинні бути розраховані у "heightForRowAtIndexPath", що викликається для кожної комірки на екрані безпосередньо перед willDisplayCell, яка встановить висоту в словнику для подальшого використання в оцінюваномуRowHeight (при перезавантаженні таблиці).
Донніт

109

Швидка версія 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

Дякую за відповідь Швидкого. У моєму випадку у мене спостерігалося деяке дивне поведінка клітин SUPER, яке виходило з ладу під час перезавантаження, коли подання таблиці прокручувалося до / біля нижньої частини. Я буду використовувати це відтепер, коли у мене є розміри комірок.
Трев14,

Прекрасно працює у Swift 4.2
Adam S.

Рятувальник життя. Так корисно при спробі додати більше елементів у джерело даних. Перешкоджає проскакуванню щойно доданих комірок до центру екрана.
Філіп Борбон

38

Стрибок через погану розрахункову висоту. Чим більше оцінювана 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 рік. На відміну від усіх інших відповідей, цей пропонує встановити оцінкуRowHeigh один раз у viewDidLoad, що допомагає, коли комірки мають однакову або дуже схожу висоту. Дякую. BTW, альтернативно esimatedRowHeight, можна встановити за допомогою інструмента інтерфейсу в Інспекторі розмірів> Перегляд таблиці> Оцінка.
Віталій

за умови, що більш точна розрахункова висота допомогла мені. Також у мене був стиль перегляду згрупованої таблиці з декількома tableView(_:estimatedHeightForHeaderInSection:)
секціями, і мені

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
працює чудово! особливо в останній комірці при перезавантаженні рядка.
Нін

19

Я спробував усі способи вирішення вище, але нічого не вийшло.

Провівши години і переживши всі можливі розчарування, придумав спосіб виправити це. Це рішення - рятівник життя! Працював як шарм!

Швидкий 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
Виправлено він, який переосмислює оцінений метод HeeightForRowAtIndexPath з високим значенням, наприклад, 300f
Flappy

1
@Flappy цікаво, як рішення, що надається вами, працює і яке коротше інших запропонованих методів. Розгляньте публікацію як відповідь.
Рохан Санап

9

Фактично ви можете перезавантажувати лише певні рядки, використовуючи reloadRowsAtIndexPaths, наприклад:

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

Але, як правило, ви також можете анімувати зміни висоти комірок таблиці таким чином:

tableView.beginUpdates()
tableView.endUpdates()

Я спробував метод startUpdates / endUpdates, але це впливає лише на видимі рядки моєї таблиці. У мене все ще виникає проблема, коли я прокручую вгору.
Девід

@David Можливо тому, що ви використовуєте орієнтовну висоту рядків.
Ліндсі Скотт

Чи варто позбутися моїх оцінокRowHeights і замість цього замінити його на початкові оновлення та endUpdates?
Девід

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

Однією з моїх проблем із спробою методу reladRowsAtIndexPaths є те, що я впроваджую нескінченну прокрутку, тому коли я перезавантажую дані, це тому, що я лише додав до джерела даних ще 15 рядків. Це означає, що вказівні шляхи для цих рядків ще не існують у UITableView
David

3

Ось трохи коротша версія:

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

3

Перевищення оціночного методу HeeightForRowAtIndexPath з високим значенням, наприклад, 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.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 оцінкуHeightForRowAt. Будьте точні, як зможете ...

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)
    }

Після того, як я додав оцінку HeeightForRowAt , більше не було стрибків.

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

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

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

Виходячи з відповідей тут, я зрозумів, що це стосується анімації, тому я виявив, що подання таблиці знаходиться всередині подання стека, а іноді ми закликаємо stackView.layoutIfNeeded()всередині анімаційного блоку. Моє остаточне рішення полягало в тому, щоб переконатися, що цей дзвінок не відбудеться, якщо "дійсно" не потрібно, тому що макет "за потреби" мав візуальну поведінку в цьому контексті, навіть коли "не потрібен".


0

У мене було те саме питання. У мене була пагинація та перезавантаження даних без анімації, але це не допомогло прокрутці, щоб запобігти стрибкам. У мене різні розміри IPhones, прокрутка не була стрибкою на iphone8, але на iphone7 +

Я застосував такі зміни у функції viewDidLoad :

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

і моя проблема вирішена. Я сподіваюся, що і вам це допоможе.


0

Один із підходів до вирішення цієї проблеми, який я знайшов, - це

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

-2

Насправді я знайшов, якщо ви використовуєте 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.