IT story

동적 셀 높이를 가진 UITableView의 reloadData ()로 인해 스크롤이 급증합니다.

hot-time 2020. 7. 12. 09:31
반응형

동적 셀 높이를 가진 UITableView의 reloadData ()로 인해 스크롤이 급증합니다.


나는 이것이 일반적인 문제라고 생각하고 그것에 대한 일반적인 해결책이 있는지 궁금합니다.

기본적으로 내 UITableView에는 모든 셀에 대한 동적 셀 높이가 있습니다. UITableView 및 I의 맨 위에 있지 않으면 위로 tableView.reloadData()스크롤이 급증합니다.

나는 이것이 데이터를 다시로드했기 때문에 스크롤 할 때 UITableView가 가시성을 얻는 각 셀의 높이를 다시 계산한다는 사실 때문이라고 생각합니다. 이를 완화하는 방법 또는 특정 IndexPath에서 UITableView의 끝까지 만 데이터를 다시로드하는 방법은 무엇입니까?

또한 맨 위로 스크롤 할 때 점프하지 않아도 문제가 없습니다. UITableViewCell 높이가 이미 계산 되었기 때문일 수 있습니다.


점프를 방지하기 위해 셀이로드 될 때 셀의 높이를 저장하고 정확한 값을 제공해야합니다 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;
}

수락 된 답변의 신속한 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 
}

점프는 예상 높이가 잘못 되었기 때문입니다. 예상 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
}

이 경우 @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
}

위의 모든 해결 방법을 시도했지만 아무 효과가 없습니다.

몇 시간을 보내고 가능한 모든 좌절을 겪고 나서 이것을 고치는 방법을 알아 냈습니다. 이 솔루션은 구세주입니다! 매력처럼 일했다!

스위프트 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. 실제로 iOS 8 전용입니다.
  2. 재정의 cellForRowAtIndexPath는 도움이되지 않습니다.

수정은 실제로 매우 간단했습니다.

재정의 estimatedHeightForRowAtIndexPath하고 올바른 값을 반환하는지 확인하십시오.

With this, all weird jittering and jumping around in my UITableViews has stopped.

NOTE: I actually know the size of my cells. There are only two possible values. If your cells are truly variable-sized, then you might want to cache the cell.bounds.size.height from tableView:willDisplayCell:forRowAtIndexPath:


You can in fact reload only certain rows by using reloadRowsAtIndexPaths, ex:

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

But, in general, you could also animate table cell height changes like so:

tableView.beginUpdates()
tableView.endUpdates()

I use more ways how to fix it:

For view controller:

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 
}

as the extension for UITableView

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

The result is

tableView.reloadSectionWithouAnimation(section: indexPath.section)

Here's a bit shorter version:

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

Overriding the estimatedHeightForRowAtIndexPath method with an high value, for example 300f

This should fix the problem :)


This one worked for me in Swift4:

extension UITableView {

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

There is a bug which I believe was introduced in iOS11.

That is when you do a reload the tableView contentOffSet gets unexpectedly altered. In fact contentOffset should not change after a reload. It tends to happen due to miscalculations of UITableViewAutomaticDimension

You have to save your contentOffSet and set it back to your saved value after your reload is finished.

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

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

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

How you use it?

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

This answer was derived from here


I have 2 different cell heights.

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

After I added estimatedHeightForRowAt, there was no more jumping.

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

Try to call cell.layoutSubviews() before returning cell in func cellForRowAtIndexPath(_ indexPath: NSIndexPath) -> UITableViewCell?. It's known bug in iOS8.


None of these solutions worked for me. Here's what I did with Swift 4 & Xcode 10.1...

In viewDidLoad(), declare table dynamic row height and create correct constraints in cells...

tableView.rowHeight = UITableView.automaticDimension

Also in viewDidLoad(), register all your tableView cell nibs to tableview like this:

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

In tableView heightForRowAt, return height equal to each cell's height at 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
    } 

}

Now give an estimated row height for each cell in tableView estimatedHeightForRowAt. Be accurate as you can...

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
    } 

}

That should work...

I didn't need to save and set contentOffset when calling tableView.reloadData()


You can use the following in 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

I had this jumping behavior and I initially was able to mitigate it by setting the exact estimated header height (because I only had 1 possible header view), however the jumps then started to happen inside the headers specifically, not affecting the whole table anymore.

Following the answers here, I had the clue that it was related to animations, so I found that the table view was inside a stack view, and sometimes we'd call stackView.layoutIfNeeded() inside an animation block. My final solution was to make sure this call doesn't happen unless "really" needed, because layout "if needed" had visual behaviors in that context even when "not needed".

참고URL : https://stackoverflow.com/questions/28244475/reloaddata-of-uitableview-with-dynamic-cell-heights-causes-jumpy-scrolling

반응형