reloadData () dari UITableView dengan ketinggian sel Dinamis menyebabkan pengguliran gelisah

142

Saya merasa seperti ini mungkin menjadi masalah umum dan bertanya-tanya apakah ada solusi umum untuk itu.

Pada dasarnya, UITableView saya memiliki ketinggian sel dinamis untuk setiap sel. Jika saya tidak berada di bagian atas UITableView dan saya tableView.reloadData(), menggulir ke atas menjadi gelisah.

Saya percaya ini karena fakta bahwa karena saya memuat ulang data, saat saya menggulir ke atas, UITableView menghitung ulang ketinggian untuk setiap sel yang terlihat. Bagaimana cara mengurangi itu, atau bagaimana saya hanya reloadData dari IndexPath tertentu ke akhir UITableView?

Selanjutnya, ketika saya berhasil menggulir ke atas, saya dapat menggulir ke bawah dan ke atas, tidak ada masalah tanpa melompat. Ini kemungkinan besar karena ketinggian UITableViewCell sudah dihitung.

David
sumber
Beberapa hal ... (1) Ya, Anda pasti dapat memuat ulang baris tertentu menggunakan reloadRowsAtIndexPaths. Tetapi (2) apa yang Anda maksud dengan "gelisah" dan (3) sudahkah Anda menetapkan tinggi baris yang diperkirakan? (Hanya mencoba mencari tahu apakah ada solusi yang lebih baik yang akan memungkinkan Anda untuk memperbarui tabel secara dinamis.)
Lyndsey Scott
@ LyndseyScott, ya, saya telah menetapkan perkiraan ketinggian baris. Maksud saya gelisah ketika saya menggulir ke atas, baris bergeser ke atas. Saya percaya ini karena saya menetapkan tinggi baris diperkirakan 128, dan kemudian ketika saya gulir ke atas, semua posting saya di atas di UITableView lebih kecil, sehingga menyusut ketinggian, menyebabkan meja saya melompat. Saya berpikir untuk melakukan reloadRowsAtIndexPaths dari baris xke baris terakhir di TableView saya ... tetapi karena saya memasukkan baris baru, itu tidak akan berfungsi, saya tidak bisa tahu apa ujung tampilan tabel saya sebelum saya memuat ulang data.
David
2
@ LyndseyScott masih saya tidak bisa menyelesaikan masalah, apakah ada solusi yang bagus?
rad
1
Apakah Anda pernah menemukan solusi untuk masalah ini? Saya mengalami masalah yang sama persis seperti yang terlihat di video Anda.
user3344977
1
Tidak ada jawaban di bawah ini yang berfungsi untuk saya.
Srujan Simha

Jawaban:

221

Untuk mencegah lompatan, Anda harus menyimpan ketinggian sel saat memuat dan memberikan nilai tepat dalam tableView:estimatedHeightForRowAtIndexPath:

Cepat:

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
}

Sasaran 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;
}
Igor
sumber
1
Terima kasih, kamu benar-benar menyelamatkan hariku :) Bekerja di objc juga
Artem Z.
3
Jangan lupa untuk menginisialisasi cellHeightsDictionary: cellHeightsDictionary = [NSMutableDictionary dictionary];
Gerharbo
1
estimatedHeightForRowAtIndexPath:mengembalikan nilai ganda dapat menyebabkan *** Assertion failure in -[UISectionRowData refreshWithSection:tableView:tableViewRowData:]kesalahan. Untuk memperbaikinya, return floorf(height.floatValue);sebagai gantinya.
liushuaikobe
Hai @ lgor, saya mengalami masalah yang sama & mencoba menerapkan solusi Anda. Masalah yang saya dapatkan adalah estimasiHeightForRowAtIndexPath dipanggil sebelum willDisplayCell, jadi tinggi sel tidak dihitung saat diperkirakanHeightForRowAtIndexPath dipanggil. Ada bantuan?
Madhuri
1
Ketinggian efektif @Madhuri harus dihitung dalam "heightForRowAtIndexPath", yang dipanggil untuk setiap sel di layar sesaat sebelum willDisplayCell, yang akan mengatur ketinggian dalam kamus untuk digunakan nanti dalam perkiraanRowHeight (pada tabel memuat ulang).
Donnit
109

Swift 3 versi jawaban yang diterima.

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 
}
Casey Wagner
sumber
Terima kasih ini berhasil! sebenarnya saya bisa menghapus implementasi saya func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {, ini menangani semua perhitungan ketinggian yang saya butuhkan.
Natalia
Setelah berjuang berjam-jam dengan lompat gigih aku tahu bahwa aku lupa menambahkan UITableViewDelegateke kelas saya. Sesuai dengan protokol itu diperlukan karena mengandung willDisplayfungsi yang ditunjukkan di atas . Saya harap saya bisa menyelamatkan seseorang dari perjuangan yang sama.
MJQZ1347
Terima kasih atas jawaban Swift. Dalam kasus saya, saya memiliki beberapa perilaku SUPER aneh sel keluar dari urutan saat memuat ulang ketika tampilan tabel digulir ke / dekat bagian bawah. Saya akan menggunakan ini mulai sekarang setiap kali saya memiliki sel ukuran sendiri.
Trev14
Bekerja dengan sempurna di Swift 4.2
Adam S.
Seorang penyelamat. Sangat membantu ketika mencoba menambahkan lebih banyak item dalam sumber data. Mencegah lompatan sel yang baru ditambahkan ke tengah layar.
Philip Borbon
38

Lompatan ini karena perkiraan ketinggian yang buruk. Semakin tinggi taksiranRowHeight berbeda dari ketinggian sebenarnya, semakin banyak tabel dapat melompat ketika dimuat ulang terutama semakin jauh ke bawah telah digulir. Ini karena ukuran perkiraan tabel secara radikal berbeda dari ukuran sebenarnya, memaksa tabel untuk menyesuaikan ukuran kontennya dan offset. Jadi estimasi ketinggian seharusnya bukan nilai acak, tetapi mendekati apa yang Anda pikirkan ketinggiannya. Saya juga pernah mengalami ketika saya mengatur UITableViewAutomaticDimension apakah sel Anda adalah tipe yang sama

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

jika Anda memiliki berbagai sel di berbagai bagian maka saya pikir tempat yang lebih baik adalah

func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
     //return different sizes for different cells if you need to
     return 100
}
Krishna Kishore
sumber
2
terima kasih, itu persis mengapa tableView saya sangat gelisah.
Louis de Decker
1
Jawaban lama, tetapi masih aktual pada 2018. Tidak seperti semua jawaban lain, yang ini menyarankan pengaturan estimasiRowHeigh sekali di viewDidLoad, yang membantu ketika sel-sel memiliki ketinggian yang sama atau sangat mirip. Terima kasih. BTW, alternatifnya diperkirakanRowHeight dapat diatur melalui Interface Builder di Size Inspector> Table View> Estimate.
Vitalii
asalkan perkiraan ketinggian yang lebih akurat membantu saya. Saya juga memiliki gaya tampilan tabel multi-bagian yang dikelompokkan, dan harus menerapkantableView(_:estimatedHeightForHeaderInSection:)
nteissler
25

@ Igor menjawab berfungsi dengan baik dalam hal ini,Swift-4kode itu.

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

dalam metode berikut 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
}
Kiran Jasvanee
sumber
6
Bagaimana cara menangani penyisipan / penghapusan baris menggunakan solusi ini? TableView melompat, karena data kamus tidak aktual.
Alexey Chekanov
1
bekerja hebat! terutama pada sel terakhir saat memuat ulang baris.
Ning
19

Saya sudah mencoba semua solusi di atas, tetapi tidak ada yang berhasil.

Setelah menghabiskan berjam-jam dan melalui semua frustrasi yang mungkin, menemukan cara untuk memperbaikinya. Solusi ini adalah penyelamat hidup! Bekerja seperti pesona!

Cepat 4

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

Saya menambahkannya sebagai ekstensi, untuk membuat kode terlihat lebih bersih dan menghindari penulisan semua baris ini setiap kali saya ingin memuat ulang.

extension UITableView {

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

akhirnya ..

tableView.reloadWithoutAnimation()

ATAU Anda benar-benar dapat menambahkan baris ini dalam UITableViewCell awakeFromNib()metode Anda

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

dan lakukan normal reloadData()

Srujan Simha
sumber
1
Bagaimana cara melakukan reload? Anda menyebutnya itu reloadWithoutAnimationtapi di mana reloadbagian?
matt
@ tikar Anda bisa menelepon tableView.reloadData()dulu dan kemudian tableView.reloadWithoutAnimation(), masih berfungsi.
Srujan Simha
Bagus! Tidak satu pun di atas tidak bekerja untuk saya juga. Bahkan semua ketinggian dan perkiraan ketinggian sama sekali. Menarik.
TY Kucuk
1
Jangan bekerja untukku. Itu macet di tableView.endUpdates (). Adakah yang bisa membantu saya!
Kakashi
12

Saya menggunakan lebih banyak cara untuk memperbaikinya:

Untuk pengontrol tampilan:

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 
}

sebagai ekstensi untuk UITableView

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

Hasilnya adalah

tableView.reloadSectionWithouAnimation(section: indexPath.section)
rastislv
sumber
1
Kuncinya bagi saya adalah menerapkan ekstensi UITableView-nya di sini. Sangat pintar. Terima kasih rastislv
BennyTheNerd
Bekerja dengan sempurna tetapi hanya memiliki satu kelemahan, Anda kehilangan animasi saat memasukkan header, footer atau baris.
Soufian Hossam
Di mana reloadSectionWithouAnimation dipanggil? Jadi misalnya, pengguna dapat memposting gambar di aplikasi saya (seperti Instagram); Saya bisa mendapatkan gambar untuk mengubah ukuran, tetapi dalam banyak kasus saya harus menggulir sel tabel dari scree untuk itu terjadi. Saya ingin sel menjadi ukuran yang benar setelah tabel melewati reloadData.
Luke Irvin
11

Saya mengalami ini hari ini dan mengamati:

  1. Hanya iOS 8 saja.
  2. Overridding cellForRowAtIndexPathtidak membantu.

Cara mengatasinya sebenarnya cukup sederhana:

Timpa estimatedHeightForRowAtIndexPathdan pastikan mengembalikan nilai yang benar.

Dengan ini, semua kegugupan dan lompatan aneh di UITableViews saya telah berhenti.

CATATAN: Saya benar-benar tahu ukuran sel saya. Hanya ada dua nilai yang mungkin. Jika sel Anda benar-benar berukuran variabel, maka Anda mungkin ingin men-cache cell.bounds.size.heightdaritableView:willDisplayCell:forRowAtIndexPath:

MarcWan
sumber
2
Memperbaikinya ketika mengesampingkan metode estimasiHeightForRowAtIndexPath dengan nilai tinggi, misalnya 300f
Flappy
1
@Flappy menarik bagaimana solusi yang disediakan oleh Anda bekerja dan lebih pendek dari teknik yang disarankan lainnya. Pertimbangkan mempostingnya sebagai jawaban.
Rohan Sanap
9

Anda sebenarnya hanya bisa memuat ulang baris tertentu dengan menggunakan reloadRowsAtIndexPaths, mis:

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

Namun, secara umum, Anda juga bisa menghidupkan perubahan tinggi sel tabel seperti:

tableView.beginUpdates()
tableView.endUpdates()
Lyndsey Scott
sumber
Saya sudah mencoba metode beginUpdates / endUpdates, tetapi itu hanya memengaruhi baris-baris tabel saya yang terlihat. Saya masih memiliki masalah ketika saya menggulir ke atas.
David
@ David Mungkin karena Anda menggunakan perkiraan ketinggian baris.
Lyndsey Scott
Haruskah saya menyingkirkan EstimatedRowHeights saya, dan sebagai gantinya menggantinya dengan beginUpdates dan endUpdates?
David
@ David Anda tidak akan "mengganti" apa pun, tetapi itu benar-benar tergantung pada perilaku yang diinginkan ... Jika Anda ingin menggunakan perkiraan tinggi baris dan hanya memuat ulang indeks di bawah bagian tabel yang terlihat saat ini, Anda dapat melakukannya seperti Saya katakan menggunakan reloadRowsAtIndexPaths
Lyndsey Scott
Salah satu masalah saya dengan mencoba metode reladRowsAtIndexPaths adalah bahwa saya menerapkan pengguliran tak terbatas, jadi ketika saya memuat ulang data itu karena saya baru saja menambahkan 15 baris lagi ke dataSource. Ini berarti bahwa indexPaths untuk baris-baris itu belum ada di UITableView
David
3

Ini versi yang sedikit lebih pendek:

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

Mengganti metode estimasiHeightForRowAtIndexPath dengan nilai tinggi, misalnya 300f

Ini harus memperbaiki masalah :)

Flappy
sumber
2

Ada bug yang saya percaya diperkenalkan di iOS11.

Saat itulah Anda melakukan reloadtableView secara contentOffSettak terduga diubah. Bahkan contentOffsetseharusnya tidak berubah setelah memuat ulang. Ini cenderung terjadi karena kesalahan perhitunganUITableViewAutomaticDimension

Anda harus menyimpan contentOffSetdan menyetelnya kembali ke nilai yang disimpan setelah memuat ulang.

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

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

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

Bagaimana Anda menggunakannya?

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

Jawaban ini berasal dari sini

Madu
sumber
2

Yang ini bekerja untuk saya di Swift4:

extension UITableView {

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

Tidak ada solusi yang berhasil untuk saya. Inilah yang saya lakukan dengan Swift 4 & Xcode 10.1 ...

Di viewDidLoad (), deklarasikan tinggi baris dinamis tabel dan buat batasan yang benar dalam sel ...

tableView.rowHeight = UITableView.automaticDimension

Juga di viewDidLoad (), daftarkan semua nibs sel tableView Anda ke tableview seperti ini:

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

Di tableView heightForRowAt, kembalikan tinggi sama dengan tinggi setiap sel di 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
    } 

}

Sekarang berikan tinggi baris perkiraan untuk setiap sel di tableView estimasiHeightForRowAt. Jadilah seakurat mungkin ...

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
    } 

}

Itu seharusnya bekerja ...

Saya tidak perlu menyimpan dan mengatur contentOffset saat memanggil tableView.reloadData ()

Michael Colonna
sumber
1

Saya memiliki 2 ketinggian sel yang berbeda.

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

Setelah saya menambahkan estimasiHeightForRowAt , tidak ada lagi lompatan.

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

Cobalah menelepon cell.layoutSubviews()sebelum mengembalikan sel func cellForRowAtIndexPath(_ indexPath: NSIndexPath) -> UITableViewCell?. Ini bug yang dikenal di iOS8.

CrimeZone
sumber
0

Anda dapat menggunakan yang berikut ini di 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
Vid
sumber
0

Saya memiliki perilaku lompatan ini dan pada awalnya saya dapat menguranginya dengan menetapkan perkiraan tinggi tajuk yang tepat (karena saya hanya memiliki 1 kemungkinan tampilan tajuk), namun lompatan kemudian mulai terjadi di dalam tajuk secara khusus, tidak memengaruhi seluruh tabel lagi.

Mengikuti jawaban di sini, saya memiliki petunjuk bahwa itu terkait dengan animasi, jadi saya menemukan bahwa tampilan tabel ada di dalam tampilan tumpukan, dan kadang-kadang kita akan memanggil stackView.layoutIfNeeded()di dalam blok animasi. Solusi terakhir saya adalah memastikan panggilan ini tidak terjadi kecuali "benar-benar" diperlukan, karena tata letak "jika diperlukan" memiliki perilaku visual dalam konteks itu bahkan ketika "tidak diperlukan".

Gobe
sumber
0

Saya memiliki masalah yang sama. Saya memiliki pagination dan memuat ulang data tanpa animasi tetapi tidak membantu gulir untuk mencegah melompat. Saya memiliki ukuran iPhone yang berbeda, gulungannya tidak gelisah pada iphone8 tetapi gelisah pada iphone7 +

Saya menerapkan perubahan berikut pada fungsi viewDidLoad :

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

dan masalah saya terpecahkan. Saya harap ini membantu Anda juga.

Burcu Kutluay
sumber
0

Salah satu pendekatan untuk memecahkan masalah ini yang saya temukan adalah

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

Sebenarnya saya menemukan jika Anda menggunakan reloadRowsmenyebabkan masalah melompat. Maka Anda harus mencoba menggunakan reloadSectionsseperti ini:

UIView.performWithoutAnimation {
    tableView.reloadSections(NSIndexSet(index: indexPath.section) as IndexSet, with: .none)
}
Michael
sumber