web-dev-qa-db-ja.com

UITableViewAutomaticDimensionでUITableViewCellを更新した後のぎくしゃくしたスクロール

ユーザーが投稿した投稿のフィードビューを持つアプリを作成しています。このビューには、カスタムUITableView実装を持つUITableViewCellがあります。このセル内には、コメントを表示するための別のUITableViewがあります。要旨は次のようなものです。

Feed TableView
  PostCell
    Comments (TableView)
      CommentCell
  PostCell
    Comments (TableView)
      CommentCell
      CommentCell
      CommentCell
      CommentCell
      CommentCell

最初のフィードはプレビュー用に3つのコメントとともにダウンロードされますが、さらにコメントがある場合、またはユーザーがコメントを追加または削除する場合は、フィードテーブルビュー内のPostCellを追加または削除して更新したいCommentCells内のコメントテーブルからPostCellを削除します。私は現在、次のヘルパーを使用してそれを実現しています。

// (PostCell.Swift) Handle showing/hiding comments
func animateAddOrDeleteComments(startRow: Int, endRow: Int, operation: CellOperation) {
  let table = self.superview?.superview as UITableView

  // "table" is outer feed table
  // self is the PostCell that is updating it's comments
  // self.comments is UITableView for displaying comments inside of the PostCell
  table.beginUpdates()
  self.comments.beginUpdates()

  // This function handles inserting/removing/reloading a range of comments
  // so we build out an array of index paths for each row that needs updating
  var indexPaths = [NSIndexPath]()
  for var index = startRow; index <= endRow; index++ {
    indexPaths.append(NSIndexPath(forRow: index, inSection: 0))
  }

  switch operation {
  case .INSERT:
    self.comments.insertRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
  case .DELETE:
    self.comments.deleteRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
  case .RELOAD:
    self.comments.reloadRowsAtIndexPaths(indexPaths, withRowAnimation: UITableViewRowAnimation.None)
  }

  self.comments.endUpdates()
  table.endUpdates()

  // trigger a call to updateConstraints so that we can update the height constraint 
  // of the comments table to fit all of the comments
  self.setNeedsUpdateConstraints()
}

override func updateConstraints() {
  super.updateConstraints()
  self.commentsHeight.constant = self.comments.sizeThatFits(UILayoutFittingCompressedSize).height
}

これにより、更新が正常に完了します。投稿は、期待どおりにPostCell内で追加または削除されたコメントで適切に更新されます。フィードテーブルでPostCellsの自動サイズ設定を使用しています。 PostCellのコメントテーブルはすべてのコメントを表示するように展開されますが、アニメーションは少しぎくしゃくしており、セルの更新アニメーションが行われている間、テーブルの並べ替えは数十ピクセル前後にスクロールします。

サイズ変更中のジャンプは少し面倒ですが、私の主な問題はその後に来ます。フィードを下にスクロールすると、スクロールは以前と同様にスムーズになりますが、コメントを追加した後にサイズを変更したばかりのセルの上にスクロールすると、フィードはフィードの上部に達する前に数回後方にジャンプします。セットアップiOS8次のようなフィードのセルの自動サイズ調整:

// (FeedController.Swift)
// tableView is the feed table containing PostCells
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 560

estimatedRowHeightを削除すると、セルの高さが変わるたびにテーブルが最上部にスクロールします。私は今これにかなり固執していると感じており、新しいiOS開発者として、あなたが持っているかもしれないあらゆるヒントを使用できます。

76
Bryan Alger

この種の問題(スクロール問題+ reloadRows + iOS 8 UITableViewAutomaticDimension)を解決するために私が見つけた最良のソリューションは次のとおりです。

TableViewがセルを表示するときに、すべての高さをディクショナリに保持し、(ディクショナリ内で)それらを更新することで構成されます。

次に、- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPathメソッドで保存した高さを返します。

次のようなものを実装する必要があります。

Objective-C

- (void)viewDidLoad {
    [super viewDidLoad];

    self.heightAtIndexPath = [NSMutableDictionary new];
    self.tableView.rowHeight = UITableViewAutomaticDimension;
}

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    NSNumber *height = [self.heightAtIndexPath objectForKey:indexPath];
    if(height) {
        return height.floatValue;
    } else {
        return UITableViewAutomaticDimension;
    }
}

- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
    NSNumber *height = @(cell.frame.size.height);
    [self.heightAtIndexPath setObject:height forKey:indexPath];
}

Swift

@IBOutlet var tableView : UITableView?
var heightAtIndexPath = NSMutableDictionary()

override func viewDidLoad() {
    super.viewDidLoad()

    tableView?.rowHeight = UITableViewAutomaticDimension
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    if let height = heightAtIndexPath.object(forKey: indexPath) as? NSNumber {
        return CGFloat(height.floatValue)
    } else {
        return UITableViewAutomaticDimension
    }
}

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    let height = NSNumber(value: Float(cell.frame.size.height))
    heightAtIndexPath.setObject(height, forKey: indexPath as NSCopying)
}
117
dosdos

同じ問題がありました。これは、SDKが不適切な高さを強制する原因となるセルの高さの不適切な推定に由来するものであり、スクロールバック時にセルがジャンプする原因となります。セルの構築方法に応じて、これを修正する最善の方法は、UITableViewDelegateメソッド- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPathを実装することです

推定値がセルの高さの実際の値にかなり近い限り、これはジャンプとジャーキネスをほぼキャンセルします。実装方法は次のとおりです。ロジックを取得できます。

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
    // This method will get your cell identifier based on your data
    NSString *cellType = [self reuseIdentifierForIndexPath:indexPath];

    if ([cellType isEqualToString:kFirstCellIdentifier])
        return kFirstCellHeight;
    else if ([cellType isEqualToString:kSecondCellIdentifier])
        return kSecondCellHeight;
    else if ([cellType isEqualToString:kThirdCellIdentifier])
        return kThirdCellHeight;
    else {
        return UITableViewAutomaticDimension;
    }
}

追加Swift 2サポート

func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    // This method will get your cell identifier based on your data
    let cellType = reuseIdentifierForIndexPath(indexPath)

    if cellType == kFirstCellIdentifier 
        return kFirstCellHeight
    else if cellType == kSecondCellIdentifier
        return kSecondCellHeight
    else if cellType == kThirdCellIdentifier
        return kThirdCellHeight
    else
        return UITableViewAutomaticDimension  
}
30
Gabriel Cartier

dosdosの答えはSwift 2で私のために働いた

Ivarを宣言します

var heightAtIndexPath = NSMutableDictionary()

func viewDidLoad()で

func viewDidLoad() {
  .... your code
  self.tableView.rowHeight = UITableViewAutomaticDimension
}

次に、次の2つのメソッドを追加します。

override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
   let height = self.heightAtIndexPath.objectForKey(indexPath)
   if ((height) != nil) {
     return CGFloat(height!.floatValue)
   } else {
    return UITableViewAutomaticDimension
   }
 }

override func tableView(tableView: UITableView, willDisplayCell cell: UITableViewCell, forRowAtIndexPath indexPath: NSIndexPath) {
  let height = cell.frame.size.height
  self.heightAtIndexPath.setObject(height, forKey: indexPath)
}

スウィフト3:

var heightAtIndexPath = [IndexPath: CGFloat]()

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

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    self.heightAtIndexPath[indexPath] = cell.frame.size.height
}
21
Ranknoodle

私も同じ問題に直面していました。私は回避策を見つけましたが、それは完全にジャークを修正しません。しかし、以前の途切れたスクロールと比較して、はるかに優れているようです。

UITableViewデリゲートメソッド:cellForRowAtIndexPath:、次の2つの方法を使用して、セルを返す前に制約を更新してみてください。 (スイフト言語)

cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()

編集:また、tableView.estimatedRowHeight値は、よりスムーズなスクロールを取得します。

2
Vishal Chandran

@ dosdos に続きます。

実装するのも面白いと思いました:tableView(tableView: didEndDisplayingCell: forRowAtIndexPath:

セルが既に画面に表示されている間に、セルが制約を動的に変更しているコード用に特に。このように辞書を更新すると、セルが2回表示されるときに役立ちます。

var heightAtIndexPath = [NSIndexPath : NSNumber]()

....

tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = UITableViewAutomaticDimension

....

extension TableViewViewController: UITableViewDelegate {

    //MARK: - UITableViewDelegate

    func tableView(tableView: UITableView,
                   estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {

        let height = heightAtIndexPath[indexPath]

        if let height = height {

            return CGFloat(height)
        }
        else {

            return UITableViewAutomaticDimension
        }
    }

    func tableView(tableView: UITableView,
                   willDisplayCell cell: UITableViewCell,
                                   forRowAtIndexPath indexPath: NSIndexPath) {

        let height: NSNumber = CGRectGetHeight(cell.frame)
        heightAtIndexPath[indexPath] = height
    }

    func tableView(tableView: UITableView,
                   didEndDisplayingCell cell: UITableViewCell,
                                        forRowAtIndexPath indexPath: NSIndexPath) {

        let height: NSNumber = CGRectGetHeight(cell.frame)
        heightAtIndexPath[indexPath] = height
    }
}
1
Gabriel.Massana

@dosdosソリューションは正常に動作しています

ただし、追加する必要があるものがあります

@dosdosの回答をフォロー中

スイフト3/4

@IBOutlet var tableView : UITableView!
var heightAtIndexPath = NSMutableDictionary()

override func viewDidLoad() {
    super.viewDidLoad()

    tableView?.rowHeight = UITableViewAutomaticDimension
}

func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
    if let height = heightAtIndexPath.object(forKey: indexPath) as? NSNumber {
        return CGFloat(height.floatValue)
    } else {
        return UITableViewAutomaticDimension
    }
}

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    let height = NSNumber(value: Float(cell.frame.size.height))
    heightAtIndexPath.setObject(height, forKey: indexPath as NSCopying)
}

それからあなたが望むときはいつでもこの行を使用してください、私のために私は内部でそれを使用しますtextDidChange

  1. 最初にTableviewをリロードします
  2. 制約の更新
  3. 最後にトップビューに移動します

    tableView.reloadData()
    self.tableView.layoutIfNeeded()
    self.tableView.setContentOffset(CGPoint.zero, animated: true)
    
0
Basil