動的なセルの高さのあるUITableViewのreloadData()が原因でスクロールが不安定になる


142

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

基本的に、私のUITableViewには、すべてのセルの動的なセルの高さがあります。UITableViewの一番上にいない場合、上にtableView.reloadData()スクロールするとびくびくします。

これは、データをリロードしたため、上にスクロールしているときに、UITableViewが表示されている各セルの高さを再計算しているためと考えられます。それをどのように軽減するか、または特定のIndexPathからUITableViewの最後までのみreloadDataをリロードする方法は?

さらに、何とか一番上までスクロールしても、下にスクロールしてから上にスクロールできます。ジャンプする必要はありません。これは、UITableViewCellの高さが既に計算されているためです。


いくつかのこと...(1)はい、を使用して特定の行を確実に再ロードできますreloadRowsAtIndexPaths。しかし、(2)「ジャンプ」とはどういう意味ですか、(3)行の推定高さを設定しましたか?(テーブルを動的に更新できるより良い解決策があるかどうかを判断するだけです。)
Lyndsey Scott

@LyndseyScott、はい、私は推定行の高さを設定しました。びくびくとは、上にスクロールすると、行が上にシフトしていることを意味します。これは、行の推定高さを128に設定し、上にスクロールすると、UITableViewの上のすべての投稿が小さくなり、高さが小さくなり、テーブルがジャンプするためと考えられます。TableViewの行xから最後の行までreloadRowsAtIndexPathsを実行することを考えています...しかし、新しい行を挿入しているため、機能しません。再ロードする前に、テーブルビューの最後がどうなるかわかりませんデータ。
デビッド

2
@LyndseyScottまだ問題を解決できません。良い解決策はありますか?
rad

1
この問題の解決策を見つけたことがありますか?あなたのビデオで見たのとまったく同じ問題が発生しています。
user3344977 2015

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
ありがとう、
Artem Z.

3
初期化することを忘れないでくださいcellHeightsDictionarycellHeightsDictionary = [NSMutableDictionary dictionary];
Gerharbo

1
estimatedHeightForRowAtIndexPath:double値を返すと、*** Assertion failure in -[UISectionRowData refreshWithSection:tableView:tableViewRowData:]エラーが発生する場合があります。return floorf(height.floatValue);代わりに、それを修正します。
liushuaikobe

こんにちは@lgor、私は同じ問題を抱えており、あなたのソリューションを実装しようとしています。私が得ている問題は、willDisplayCellの前に見積もられたHeightForRowAtIndexPathが呼び出されるため、stimulatedHeightForRowAtIndexPathが呼び出されたときにセルの高さが計算されないことです。何か助けは?
Madhuri 2017

1
@Madhuriの有効な高さは、 "heightForRowAtIndexPath"で計算する必要があります。これは、willDisplayCellの直前の画面上のすべてのセルに対して呼び出されます。
Donnit

109

受け入れられた回答の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 
}

ありがとうございます。実際、の実装を削除することができましたfunc tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {。これにより 、必要なすべての高さ計算が処理されます。
ナタリア

持続的なジャンプで何時間も苦労した後UITableViewDelegate、クラスに追加するのを忘れていることに気付きました。上記のwillDisplay機能が含まれているため、このプロトコルへの準拠は必須です。私は誰かが同じ苦労を救えることを願っています。
MJQZ1347 2017

Swiftの回答ありがとうございます。私の場合、テーブルビューが下部にスクロールされたとき、またはその近くにスクロールされたときに、リロード時にセルの順序が狂うという超奇妙な動作がありました。これからは、セルのサイズを変更するたびにこれを使用します。
Trev14

Swift 4.2で完全に動作します
Adam S.

命の恩人。データソースにさらにアイテムを追加しようとするときにとても便利です。新しく追加されたセルが画面の中央にジャンプしないようにします。
フィリップボルボン

38

ジャンプは悪い推定高さのためです。見積もられた行の高さが実際の高さと異なるほど、再ロードされたときにテーブルがジャンプする可能性が高くなります(特に、下にスクロールされた場合)。これは、テーブルの推定サイズが実際のサイズと根本的に異なり、テーブルにコンテンツのサイズとオフセットを調整させるためです。したがって、推定される高さはランダムな値であってはなりませんが、高さが予想される値に近くなります。私は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がとてもびくびくした理由です。
Louis de Decker

1
他のすべての回答とは異なり、これはviewDidLoadで推定行高さを一度設定することをお勧めします。これは、セルの高さが同じか非常に似ている場合に役立ちます。ありがとう。ところで、esimatedRowHeightは、サイズインスペクター>テーブルビュー>見積もりの​​インターフェイスビルダーで設定することもできます。
Vitalii 2018年

より正確な推定された高さが私に役立ちました。また、複数セクションのグループ化されたテーブルビュースタイルがあり、実装しなければなり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はジャンプします。
Alexey Chekanov

1
よく働く!特に、行をリロードするときの最後のセルで。
Ning

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 2018

1
私のために働かないでください。tableView.endUpdates()でクラッシュします。誰かが私を助けてくれますか?
カカシ

12

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

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の拡張として

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

完全に機能しますが、欠点は1つだけです。ヘッダー、フッター、または行を挿入すると、アニメーションが失われます。
Soufian Hossam

reloadSectionWithouAnimationはどこで呼び出されますか?たとえば、ユーザーは私のアプリ(Instagramなど)に画像を投稿できます。画像のサイズを変更することはできますが、そのためにはほとんどの場合、テーブルセルを画面からスクロールして外さなければなりません。テーブルがreloadDataを通過したら、セルを正しいサイズにしたいと思います。
ルークアーヴィン

11

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

  1. 実際、iOS 8のみです。
  2. オーバーライドcellForRowAtIndexPathは役に立ちません。

修正は実際にはかなり簡単でした:

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

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

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


2
例えば300Fのために、高い値のestimatedHeightForRowAtIndexPathメソッドをオーバーライドWENそれを修正
ゆるい

1
@Flappyあなたが提供するソリューションがどのように機能するかは興味深いもので、他の提案されたテクニックよりも短いです。回答として投稿することを検討してください。
Rohan Sanap

9

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

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

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

tableView.beginUpdates()
tableView.endUpdates()

beginUpdates / endUpdatesメソッドを試しましたが、これはテーブルの表示されている行にのみ影響します。上にスクロールしてもまだ問題があります。
デビッド

@Davidおそらく、推定された行の高さを使用しているためです。
リンジースコット2015

EstimatedRowHeightsを削除して、代わりにbeginUpdatesとendUpdatesで置き換える必要がありますか?
デビッド

@David何かを「置き換える」ことはないでしょうが、それは実際に望ましい動作に依存します...推定された行の高さを使用して、テーブルの現在表示されている部分の下にインデックスを再ロードしたい場合は、次のようにすることができますreloadRowsAtIndexPathsを使用すると言った
Lyndsey Scott

reladRowsAtIndexPathsメソッドを試す際の問題の1つは、無限スクロールを実装していることです。そのため、データをreloadingしているときは、dataSourceに15行を追加しただけです。これは、これらの行のindexPathがUITableViewにまだ存在していないことを意味します
David

3

これは少し短いバージョンです:

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

3

高い値(300fなど)で見積もられたHeightForRowAtIndexPathメソッドをオーバーライドする

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


2

iOS11で導入されたと思われるバグがあります。

そのとき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)

この答えはここから導き出されました


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セルnibを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
    } 

}

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

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


1

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

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

見積もられたHeightForRowAtを追加した後、それ以上ジャンプすることはありませんでした。

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

私が見つけたこの問題を解決するためのアプローチの1つは

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)
}
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.