web-dev-qa-db-ja.com

ダイナミックセルの高さを持つUITableViewのreloadData()により、スクロールが急激に変化する

これは一般的な問題であり、一般的な解決策があるかどうか疑問に思っていました。

基本的に、私のUITableViewにはすべてのセルの動的なセルの高さがあります。 UITableViewの上部にいないときにtableView.reloadData()を押すと、上にスクロールするとびびります。

これは、スクロールしているときにデータをリロードしたため、UITableViewが表示される各セルの高さを再計算しているためだと思います。どのようにそれを緩和するのですか、または特定のIndexPathからUITableViewの最後までデータをリロードするだけですか?

さらに、一番上までスクロールできた場合、下にスクロールしてから上にスクロールできます。ジャンプすることは問題ありません。これはおそらく、UITableViewCellの高さがすでに計算されているためです。

113
David

ジャンプを防ぐには、セルが読み込まれるときにセルの高さを保存し、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;
}
175
Igor

受け入れられた答えの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 
}
96
Casey Wagner

ジャンプは、推定高さが悪いためです。 timateedRowHeightが実際の高さと異なるほど、テーブルが再ロードされたときにジャンプする可能性が高くなり、特にスクロールされた場合はさらに下になります。これは、テーブルの推定サイズが実際のサイズとは根本的に異なり、コンテンツのサイズとオフセットをテーブルに強制的に調整させるためです。そのため、推定される高さはランダムな値ではなく、高さが予想される値に近いはずです。あなたのセルが同じタイプの場合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
}
35
Krishna Kishore

@ 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
}
25
Kiran Jasvanee

上記のすべての回避策を試しましたが、何も機能しませんでした。

数時間を費やし、考えられるすべてのフラストレーションを経験した後、これを修正する方法を見つけました。このソリューションは命の恩人です!魅力のように働いた!

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

または実際にこれらの行をUITableViewCellawakeFromNib()メソッドに追加できます

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

そして、通常のreloadData()を行います

17
Srujan Simha

今日私はこれに遭遇し、観察しました:

  1. 確かにiOS 8のみです。
  2. cellForRowAtIndexPathを上書きしても効果はありません。

修正は実際には非常に簡単でした:

estimatedHeightForRowAtIndexPathをオーバーライドし、正しい値が返されることを確認します。

これにより、UITableViewsでの奇妙なジッタリングやジャンプはすべて停止しました。

注:私は実際に私の細胞のサイズを知っています。可能な値は2つだけです。セルが本当に可変サイズの場合、cell.bounds.size.heightからtableView:willDisplayCell:forRowAtIndexPath:をキャッシュすることをお勧めします

11
MarcWan

私はそれを修正するより多くの方法を使用します:

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 
}

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

実際、reloadRowsAtIndexPathsを使用して特定の行のみを再ロードできます。例:

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

ただし、一般に、次のようにテーブルセルの高さの変化をアニメーション化することもできます。

tableView.beginUpdates()
tableView.endUpdates()
7
Lyndsey Scott

ここに少し短いバージョンがあります:

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

これはSwift4で私のために働いた:

extension UITableView {

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

ExpectedHeightForRowAtIndexPathメソッドを高い値(300fなど)でオーバーライドする

これで問題が解決するはずです:)

2
Flappy

2つの異なるセルの高さがあります。

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

estimatedHeightForRowAtを追加した後、これ以上のジャンプはありませんでした。

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

IOS11で導入されたと思われるbugがあります。

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)

この答えは here から派生しました

1
Honey

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
Vid

これらのソリューションはどれも私にとってはうまくいきませんでした。 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
    } 

}

次に、tableViewtimatedHeightForRowAtの各セルの推定行の高さを指定します。できる限り正確に...

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
    } 

}

それはうまくいくはずです...

TableView.reloadData()を呼び出すときにcontentOffsetを保存して設定する必要はありませんでした

0
Michael Colonna

cell.layoutSubviews()のセルを返す前に、func cellForRowAtIndexPath(_ indexPath: NSIndexPath) -> UITableViewCell?を呼び出してみてください。 iOS8の既知のバグです。

0
CrimeZone