reloadData () elementu UITableView z dynamicznymi wysokościami komórek powoduje gwałtowne przewijanie


142

Czuję, że może to być powszechny problem i zastanawiałem się, czy istnieje jakieś wspólne rozwiązanie tego problemu.

Zasadniczo mój UITableView ma dynamiczne wysokości komórek dla każdej komórki. Jeśli nie jestem na górze UITableView i ja tableView.reloadData(), przewijanie w górę staje się skokowe.

Uważam, że wynika to z faktu, że ponieważ ponownie ładowałem dane, gdy przewijam w górę, UITableView ponownie oblicza wysokość każdej komórki, która staje się widoczna. Jak to złagodzić lub jak mogę tylko przeładować dane z określonego IndexPath na koniec UITableView?

Co więcej, kiedy uda mi się przewinąć całą drogę do góry, mogę przewijać z powrotem w dół, a następnie w górę, bez problemu bez przeskakiwania. Jest to najprawdopodobniej spowodowane już obliczeniem wysokości UITableViewCell.


Kilka rzeczy ... (1) Tak, zdecydowanie możesz ponownie załadować niektóre wiersze za pomocą reloadRowsAtIndexPaths. Ale (2) co masz na myśli przez „skokowy” i (3) czy ustawiłeś szacowaną wysokość wiersza? (Próbuję tylko dowiedzieć się, czy istnieje lepsze rozwiązanie, które pozwoliłoby dynamicznie aktualizować tabelę.)
Lyndsey Scott

@LyndseyScott, tak, ustawiłem szacunkową wysokość wiersza. Przez skakanie mam na myśli to, że kiedy przewijam w górę, rzędy przesuwają się w górę. Wydaje mi się, że dzieje się tak, ponieważ ustawiam szacunkową wysokość wiersza na 128, a następnie, gdy przewijam w górę, wszystkie moje posty powyżej w UITableView są mniejsze, więc zmniejsza wysokość, powodując przeskakiwanie mojej tabeli. Myślę o zrobieniu reloadRowsAtIndexPaths z wiersza xdo ostatniego wiersza w moim TableView ... ale ponieważ wstawiam nowe wiersze, to nie zadziała, nie mogę wiedzieć, jaki będzie koniec mojego widoku tabeli przed ponownym załadowaniem dane.
David

2
@LyndseyScott nadal nie mogę rozwiązać problemu, czy jest jakieś dobre rozwiązanie?
rad

1
Czy kiedykolwiek znalazłeś rozwiązanie tego problemu? Mam dokładnie ten sam problem, co w Twoim filmie.
user3344977

1
Żadna z poniższych odpowiedzi nie pomogła.
Srujan Simha

Odpowiedzi:


221

Aby zapobiec skokom, należy zapisać wysokość komórek podczas ich ładowania i podać dokładną wartość w tableView:estimatedHeightForRowAtIndexPath:

Szybki:

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
}

Cel 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
Dzięki, naprawdę ratujesz mi dzień :) Pracuje też w objc
Artem Z.

3
Nie zapomnij o inicjalizacji cellHeightsDictionary: cellHeightsDictionary = [NSMutableDictionary dictionary];
Gerharbo,

1
estimatedHeightForRowAtIndexPath:zwraca podwójną wartość może spowodować *** Assertion failure in -[UISectionRowData refreshWithSection:tableView:tableViewRowData:]błąd. return floorf(height.floatValue);Zamiast tego , żeby to naprawić .
liushuaikobe

Cześć @lgor, mam ten sam problem i próbuję wdrożyć Twoje rozwiązanie. Problem, który otrzymuję, jest szacowany, gdy wywoływana jest metoda validHeightForRowAtIndexPath, która jest wywoływana przed willDisplayCell, więc wysokość komórki nie jest obliczana. Jakaś pomoc?
Madhuri,

1
Efektywne wysokości @Madhuri powinny być obliczane w "heightForRowAtIndexPath", które jest wywoływane dla każdej komórki na ekranie tuż przed willDisplayCell, które ustawi wysokość w słowniku do późniejszego wykorzystania w oszacowaniuRowHeight (przy przeładowaniu tabeli).
Donnit

109

Szybka 3 wersja zaakceptowanej odpowiedzi.

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 
}

Dzięki, że działało świetnie! w rzeczywistości udało mi się usunąć moją implementację func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {, która obsługuje wszystkie obliczenia wysokości, których potrzebuję.
Natalia

Po wielu godzinach zmagań z wytrwałymi skokami stwierdziłem, że zapomniałem dodać coś UITableViewDelegatedo swojej klasy. Zgodność z tym protokołem jest konieczna, ponieważ zawiera on powyższą willDisplayfunkcję. Mam nadzieję, że uda mi się uratować komuś tę samą walkę.
MJQZ1347,

Dziękuję za szybką odpowiedź. W moim przypadku miałem SUPER dziwne zachowanie komórek, które nie działały po przeładowaniu, gdy widok tabeli był przewijany do / blisko dołu. Będę go używać od teraz, ilekroć będę miał komórki samodopasowujące.
Trev14

Działa idealnie w Swift 4.2
Adam S.

Ratownik życia. Jest to przydatne, gdy próbujesz dodać więcej elementów do źródła danych. Zapobiega przeskakiwaniu nowo dodanych komórek na środek ekranu.
Philip Borbon

38

Skok jest spowodowany złą oceną wysokości. Im bardziej szacunkowa wysokośćRowHeight różni się od rzeczywistej wysokości, tym bardziej tabela może podskakiwać po przeładowaniu, zwłaszcza im dalej w dół została przewinięta. Dzieje się tak, ponieważ szacowany rozmiar tabeli radykalnie różni się od jej rzeczywistego rozmiaru, co wymusza na tabeli dostosowanie rozmiaru zawartości i przesunięcia. Więc szacowana wysokość nie powinna być wartością przypadkową, ale zbliżoną do tego, co myślisz, że będzie. Doświadczyłem również, kiedy ustawiam, UITableViewAutomaticDimension czy twoje komórki są wtedy tego samego typu

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

jeśli masz różne komórki w różnych sekcjach, myślę, że jest lepsze miejsce

func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
     //return different sizes for different cells if you need to
     return 100
}

2
dziękuję, właśnie dlatego mój tableView był taki nerwowy.
Louis de Decker

1
Stara odpowiedź, ale nadal aktualna od 2018 r. W przeciwieństwie do wszystkich innych odpowiedzi, ta sugeruje ustawienie parametru previousRowHeigh po wyświetleniu DidLoad, co pomaga, gdy komórki mają taką samą lub bardzo podobną wysokość. Dzięki. BTW, alternatywnie esimatedRowHeight można ustawić za pomocą narzędzia Interface Builder w menu Size Inspector> Table View> Estimate.
Vitalii

pod warunkiem, że pomogła mi dokładniejsza szacowana wysokość. Miałem też wieloczęściowy zgrupowany styl widoku tabeli i musiałem zaimplementowaćtableView(_:estimatedHeightForHeaderInSection:)
nteissler

25

@Igor odpowiedź działa dobrze w tym przypadku,Swift-4kod.

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

w następujących metodach 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
Jak radzić sobie z wstawianiem / usuwaniem wierszy za pomocą tego rozwiązania? TableView przeskakuje, ponieważ dane słownika nie są rzeczywiste.
Alexey Chekanov

1
działa świetnie! szczególnie w ostatniej komórce po przeładowaniu wiersza.
Ning

19

Wypróbowałem wszystkie powyższe obejścia, ale nic nie działało.

Po spędzeniu wielu godzin i przejściu przez wszystkie możliwe frustracje, wymyśliłem sposób, aby to naprawić. To rozwiązanie ratuje życie! Działał jak urok!

Szybki 4

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

Dodałem go jako rozszerzenie, aby kod wyglądał bardziej przejrzysto i unikałem pisania tych wszystkich wierszy za każdym razem, gdy chcę przeładować.

extension UITableView {

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

Wreszcie ..

tableView.reloadWithoutAnimation()

LUB możesz dodać te linie do swojej UITableViewCell awakeFromNib()metody

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

i normalnie reloadData()


1
Jak to się dzieje z ponownym ładowaniem? Ty nazywają go reloadWithoutAnimation, ale gdzie jest reloadczęść?
mat.

@matt możesz tableView.reloadData()najpierw zadzwonić, a potem tableView.reloadWithoutAnimation()nadal działa.
Srujan Simha

Wspaniały! Żadne z powyższych nie działało również dla mnie. Nawet wszystkie wysokości i szacowane wysokości są całkowicie takie same. Ciekawy.
TY Kucuk,

1
Nie pracuj dla mnie. Występuje awaria w tableView.endUpdates (). Czy ktoś może mi pomóc!
Kakashi

12

Używam więcej sposobów, jak to naprawić:

W przypadku kontrolera widoku:

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 
}

jako rozszerzenie dla UITableView

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

Wynik to

tableView.reloadSectionWithouAnimation(section: indexPath.section)

1
Kluczem dla mnie było zaimplementowanie jego rozszerzenia UITableView tutaj. Bardzo mądry. Dzięki rastislv
BennyTheNerd

Działa idealnie, ale ma tylko jedną wadę, tracisz animację podczas wstawiania nagłówka, stopki lub wiersza.
Soufian Hossam

Gdzie można by nazwać reloadSectionWithouAnimation? Na przykład użytkownicy mogą publikować obraz w mojej aplikacji (np. Na Instagramie); Mogę zmienić rozmiar obrazów, ale w większości przypadków muszę przewinąć komórkę tabeli poza osypisko, aby tak się stało. Chcę, aby komórka miała prawidłowy rozmiar po przejściu tabeli przez reloadData.
Luke Irvin

11

Wpadłem na to dzisiaj i zauważyłem:

  1. Rzeczywiście jest to tylko iOS 8.
  2. Overridding cellForRowAtIndexPathnie pomaga.

Poprawka była całkiem prosta:

Zastąp estimatedHeightForRowAtIndexPathi upewnij się, że zwraca prawidłowe wartości.

Dzięki temu wszystkie dziwne drgania i skakanie w moich UITableViews ustały.

UWAGA: Właściwie znam rozmiar moich komórek. Istnieją tylko dwie możliwe wartości. Jeśli twoje komórki mają naprawdę zmienną wielkość, możesz chcieć buforować plik cell.bounds.size.heightfromtableView:willDisplayCell:forRowAtIndexPath:


2
Naprawiono to, gdy zastępowano metodę oszacowanąHeightForRowAtIndexPath wysoką wartością, na przykład 300f
Flappy

1
@Flappy to ciekawe, jak działa dostarczone przez Ciebie rozwiązanie i jest krótsze niż inne sugerowane techniki. Rozważ opublikowanie tego jako odpowiedzi.
Rohan Sanap

9

W rzeczywistości możesz przeładować tylko niektóre wiersze za pomocą reloadRowsAtIndexPaths, na przykład:

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

Ale ogólnie możesz także animować zmiany wysokości komórek tabeli w następujący sposób:

tableView.beginUpdates()
tableView.endUpdates()

Wypróbowałem metodę beginUpdates / endUpdates, ale ma to wpływ tylko na widoczne wiersze mojej tabeli. Nadal mam problem, kiedy przewijam w górę.
David

@David Prawdopodobnie dlatego, że używasz szacowanych wysokości wierszy.
Lyndsey Scott

Czy powinienem pozbyć się moich EstimatedRowHeights i zamiast tego zamienić je na beginUpdates i endUpdates?
David

@David Nie byłbyś niczego "zastępował", ale to naprawdę zależy od pożądanego zachowania ... Jeśli chcesz użyć szacowanej wysokości wierszy i po prostu przeładować indeksy poniżej aktualnie widocznej części tabeli, możesz to zrobić tak jak Powiedziałem, używając reloadRowsAtIndexPaths
Lyndsey Scott

Jednym z moich problemów z wypróbowaniem metody reladRowsAtIndexPaths jest to, że wdrażam nieskończone przewijanie, więc kiedy przeładowuję dane, dzieje się tak, ponieważ właśnie dodałem 15 kolejnych wierszy do źródła danych. Oznacza to, że indexPaths dla tych wierszy jeszcze nie istnieje w UITableView
David

3

Oto nieco krótsza wersja:

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

3

Zastępowanie metodyimateHeightForRowAtIndexPath wysoką wartością, na przykład 300f

To powinno rozwiązać problem :)


2

Jest błąd, który, jak sądzę, został wprowadzony w iOS11.

Dzieje się tak, gdy robisz, a reloadtableView contentOffSetzostaje nieoczekiwanie zmienione. W rzeczywistości contentOffsetnie powinno się zmieniać po przeładowaniu. Zwykle dzieje się tak z powodu błędnych obliczeńUITableViewAutomaticDimension

Musisz zapisać swoją contentOffSetwartość i ustawić ją z powrotem na zapisaną wartość po zakończeniu przeładowania.

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

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

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

Jak tego używasz?

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

Ta odpowiedź pochodzi stąd


2

Ten pracował dla mnie w Swift4:

extension UITableView {

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

1

Żadne z tych rozwiązań nie działało dla mnie. Oto, co zrobiłem ze Swift 4 i Xcode 10.1 ...

W viewDidLoad () zadeklaruj dynamiczną wysokość wiersza tabeli i utwórz poprawne ograniczenia w komórkach ...

tableView.rowHeight = UITableView.automaticDimension

Również w viewDidLoad () zarejestruj wszystkie końcówki komórek tableView w widoku tabeli w następujący sposób:

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

W tableView heightForRowAt zwraca wysokość równą wysokości każdej komórki w 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
    } 

}

Teraz podaj szacowaną wysokość wiersza dla każdej komórki w tableView EstymatedHeightForRowAt. Bądź dokładny, jak potrafisz ...

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
    } 

}

To powinno działać...

Nie musiałem zapisywać i ustawiać contentOffset podczas wywoływania tableView.reloadData ()


1

Mam 2 różne wysokości komórek.

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

Po dodaniu szacunkowej ocenyHeightForRowAt nie było więcej skoków.

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

0

Spróbuj zadzwonić cell.layoutSubviews()przed powrotem do komórki func cellForRowAtIndexPath(_ indexPath: NSIndexPath) -> UITableViewCell?. To znany błąd w iOS8.


0

Możesz użyć następujących w 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

Miałem to zachowanie związane z skokami i początkowo byłem w stanie je złagodzić, ustawiając dokładną szacowaną wysokość nagłówka (ponieważ miałem tylko 1 możliwy widok nagłówka), jednak skoki zaczęły się wtedy pojawiać szczególnie wewnątrz nagłówków, nie wpływając już na całą tabelę.

Podążając za odpowiedziami tutaj, miałem wskazówkę, że jest to związane z animacjami, więc odkryłem, że widok tabeli znajduje się wewnątrz widoku stosu, a czasami wywoływaliśmy stackView.layoutIfNeeded()wewnątrz bloku animacji. Ostatnim rozwiązaniem było upewnienie się, że to wywołanie nie nastąpi, chyba że jest to „naprawdę” potrzebne, ponieważ układ „w razie potrzeby” zachowuje się wizualnie w tym kontekście, nawet gdy „nie jest potrzebny”.


0

Miałem ten sam problem. Miałem paginację i ponowne ładowanie danych bez animacji, ale nie pomogło to przewijaniu zapobiec przeskakiwaniu. Mam różne rozmiary iPhone'ów, przewijanie nie było skokowe na iphone8, ale na iphone7 +

Zastosowałem następujące zmiany w funkcji viewDidLoad :

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

i mój problem został rozwiązany. Mam nadzieję, że tobie też pomoże.


0

Jednym ze sposobów rozwiązania tego problemu, które znalazłem, jest

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

-2

Właściwie znalazłem, jeśli używasz reloadRowspowodując problem ze skokiem. Następnie powinieneś spróbować użyć w reloadSectionsten sposób:

UIView.performWithoutAnimation {
    tableView.reloadSections(NSIndexSet(index: indexPath.section) as IndexSet, with: .none)
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.