使用UITableViewAutomaticDimension在适当位置更新UITableViewCell之后的滚动滚动


78

我正在构建一个具有供用户提交的帖子的供稿视图的应用。此视图具有UITableView一个自定义UITableViewCell实现。在此单元格中,我还有另一个UITableView用于显示评论。要点是这样的:

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

初始Feed将下载3条评论进行预览,但是如果有更多评论,或者用户添加或删除评论,我想PostCell通过在其中添加或删除CommentCells评论表来更新Feed表视图中的的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在Feed表中使用了自动调整大小。展开后的注释表PostCell可以显示所有注释,但是动画有点生涩,在进行单元格更新动画时,该表会上下滚动十几个像素。

调整大小过程中的跳跃有点烦人,但是我的主要问题随后出现。现在,如果我在提要中向下滚动,则滚动将像以前一样平滑,但是如果我在添加注释后重新调整大小的单元格上方向上滚动,则提要将向后跳转几次,然后到达提要的顶部。我iOS8为Feed设置了自动调整大小的单元格,如下所示:

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

如果删除estimatedRowHeight,则只要单元格高度发生变化,表格就会滚动到顶部。我现在对此感到非常困惑,作为一名新的iOS开发人员,可以使用您可能拥有的所有技巧。

Answers:


120

这是我找到的解决此类问题的最佳解决方案(滚动问题+ reloadRows + iOS 8 UITableViewAutomaticDimension);

它包括将每个高度保留在字典中并更新(在字典中),因为tableView将显示单元格。

然后,您将在- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath方法中返回保存的高度。

您应该实现以下内容:

目标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];
}

迅捷3

@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)
}

2
真正简单有效的解决方案。谢谢!
o15a3d4l11s2 '02

1
只是好奇,我有什么理由不能使用Swift字典而不是NSMutableDictionary吗?顺便说一下,此解决方案效果很好,谢谢!
AppreciateIt'6

3
@dosdos上帝保佑你,老兄!
adnako '16

5
对我来说,它仍然不起作用,我有一个自定义的imageView,并根据宽度和高度的尺寸来更新其对高度的约束。即使缓存了单元格的高度,滚动仍在跳跃,特别是在将一个新的高度与屏幕上当前单元格不同的新单元格滚动到屏幕上之前。
TonyTony

1
极好!精彩!运行的很好!!!!非常感谢!我认为您可以将其标记为解决方案。
ndominati2

30

我们有同样的问题。这是因为对单元格高度的错误估计导致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  
}

1
我最终实现了heightForRowAtIndexPath方法并缓存结果以提高性能,因为它涉及到一点并且提要可能很长。当用户在提要中的一个帖子上添加/删除或加载评论时,我会使该单元格的高度计算无效,以便在滚动过程中重新计算它。混蛋已经过去了,我希望我可以简化代码并利用新的高度计算功能,但是我无法使其与TableViewCell一起工作得足够好
Bryan Alger

2
您是否尝试过上述方法?这是应该在iOS 8上完成的方式,不应计算高度,因为框架已经在处理它。如果实现heightForRowAtIndexPath方法,则仅是覆盖SDK的行为。
加百利·卡地亚

7
@BryanAlger是正确的。自动行高不适用于行数很多且高度差异很大的表。为了获得可靠的平滑滚动,您必须在heightForRowAtIndexPath方法中给出正确的结果,最好使用缓存的行高。否则,当tableView需要更新它的contentSize时,您将变得很笨拙,尤其是在您按下或显示另一个视图控制器并返回时。不幸的是,自动行高仅适用于具有几行的简单tableView。
Jamie Hamick

2
在实现heightForRowAtIndexPath的情况下,您没有使用自动像元高度标注的功能。当然,可以实现,但是您的单元不是动态的。
加百利·卡地亚

1
就我而言,我已经在中实现了单元格配置,高度计算和高度缓存heightForRowAtIndexPath,但是UITableView滚动还是比较混乱。遵循@GabrielCartier的答案,并根据单元的类型添加更多特定的逻辑确实有助于解决该问题,谢谢!
Sakiboy

23

dosdos答案在Swift 2中为我工作

声明ivar

var heightAtIndexPath = NSMutableDictionary()

在func viewDidLoad()中

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

然后添加以下两种方法:

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

SWIFT 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
}

1
谢谢!很棒:)
迈克尔•迈克尔(Michael

很棒,完全消除了闪烁。
AVEbrahimi

添加了SWIFT 3的更新(请不要忘记viewDidLoad中的self.tableView.rowHeight = UITableViewAutomaticDimension)
MLBDG

不幸的是,这对我并没有真正的帮助。自动布局在这里有点奇怪。
nickdnk

1
嘿,我编辑了您的答案以使用键入的字典。我只是想粘贴自己的Swift 4代码作为答案,但发现编辑您的代码就足够了。希望你不要介意。
manmal

3

@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

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

2

我也面临着同样的问题。我确实找到了一种解决方法,但是并不能完全解决问题。但是,与之前的断断续续滚动相比,它似乎要好得多。

在您的UITableView委托方法中:cellForRowAtIndexPath:,尝试使用以下两种方法在返回单元格之前更新约束。(快速语言)

cell.setNeedsUpdateConstraints()
cell.updateConstraintsIfNeeded()

编辑:您可能还必须尝试使用​​该tableView.estimatedRowHeight值才能获得更平滑的滚动。


5
我不建议使用此方法,在诸如cellForRowAtIndexPath这样的方法中调用自动布局方法会极大地影响TableView的性能。
加百利·卡地亚

1

以下@dosdos答案。

我还发现有趣的实现方式: tableView(tableView: didEndDisplayingCell: forRowAtIndexPath:

特别是对于我的代码,当该单元格已经显示在屏幕上时,该单元格正在动态更改约束。像这样更新字典有助于第二次显示该单元格。

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
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.