iOS 10 中的 UITableViewCell 动画高度问题

UITableViewCell animate height issue in iOS 10

我的 UITableViewCell 会在识别点击时为其高度设置动画。在 iOS 9 及以下版本中,此动画流畅且工作正常。在 iOS 10 beta 中,动画期间有一个不和谐的跳跃。有办法解决这个问题吗?

这是代码的基本示例。

- (void)cellTapped {
    [self.tableView beginUpdates];
    [self.tableView endUpdates];
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return self.shouldExpandCell ? 200.0f : 100.0f;
}

编辑:2016 年 9 月 8 日

GM中问题依然存在。经过更多调试后,我发现问题与单元格立即跳到新高度有关,然后将动画相关单元格偏移量。这意味着任何依赖于单元格底部的基于 CGRect 的动画都将不起作用。

例如,如果我将视图限制在单元格底部,它将跳转。该解决方案将涉及使用动态常数对顶部的约束。或者想出另一种方式来定位你的观点,而不是与底部相关。

更好的解决方案:

问题是当 UITableView 更改单元格的高度时,很可能来自 -beginUpdates-endUpdates。在 iOS 10 之前,sizeorigin 都会在单元格上显示动画。现在,在 iOS 10 GM 中,单元格将立即更改为新的高度,然后动画到正确的偏移量。

解决方案非常简单,但有限制。创建一个指南约束,它将更新它的高度,并将其他需要约束到单元格底部的视图,现在约束到这个指南。

- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        UIView *heightGuide = [[UIView alloc] init];
        heightGuide.translatesAutoresizingMaskIntoConstraints = NO;
        [self.contentView addSubview:heightGuide];
        [heightGuide addConstraint:({
            self.heightGuideConstraint = [NSLayoutConstraint constraintWithItem:heightGuide attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:0.0f];
        })];
        [self.contentView addConstraint:({
            [NSLayoutConstraint constraintWithItem:heightGuide attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeTop multiplier:1.0f constant:0.0f];
        })];

        UIView *anotherView = [[UIView alloc] init];
        anotherView.translatesAutoresizingMaskIntoConstraints = NO;
        anotherView.backgroundColor = [UIColor redColor];
        [self.contentView addSubview:anotherView];
        [anotherView addConstraint:({
            [NSLayoutConstraint constraintWithItem:anotherView attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0f constant:20.0f];
        })];
        [self.contentView addConstraint:({
            [NSLayoutConstraint constraintWithItem:anotherView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeLeft multiplier:1.0f constant:0.0f];
        })];
        [self.contentView addConstraint:({
            // This is our constraint that used to be attached to self.contentView
            [NSLayoutConstraint constraintWithItem:anotherView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:heightGuide attribute:NSLayoutAttributeBottom multiplier:1.0f constant:0.0f];
        })];
        [self.contentView addConstraint:({
            [NSLayoutConstraint constraintWithItem:anotherView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.contentView attribute:NSLayoutAttributeRight multiplier:1.0f constant:0.0f];
        })];
    }
    return self;
}

然后在需要时更新参考线高度。

- (void)setFrame:(CGRect)frame {
    [super setFrame:frame];

    if (self.window) {
        [UIView animateWithDuration:0.3 animations:^{
            self.heightGuideConstraint.constant = frame.size.height;
            [self.contentView layoutIfNeeded];
        }];

    } else {
        self.heightGuideConstraint.constant = frame.size.height;
    }
}

请注意,将更新指南放在 -setFrame: 中可能不是最佳位置。到目前为止,我只构建了这个演示代码来创建解决方案。使用最终解决方案完成代码更新后,如果我找到更好的放置位置,我将进行编辑。

原答案:

随着 iOS 10 测试版接近完成,希望这个问题将在下一个版本中得到解决。还有一个开放的错误报告。

我的解决方案涉及到图层的 CAAnimation。这将检测高度的变化并自动设置动画,就像使用未链接到 UIView.

CALayer

第一步是调整图层检测到变化时发生的情况。此代码必须在您的视图的子类中。该视图必须是您的 tableViewCell.contentView 的子视图。在代码中,我们检查视图层的动作 属性 是否有我们动画的键。如果不只是调用 super.

- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
    return [layer.actions objectForKey:event] ?: [super actionForLayer:layer forKey:event];
}

接下来您要将动画添加到动作中 属性。您可能会发现此代码最好在视图位于 window 上并进行布局后应用。预先应用它可能会在视图移动到 window.

时产生动画
- (void)didMoveToWindow {
    [super didMoveToWindow];

    if (self.window) {
        CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"bounds"];
        self.layer.actions = @{animation.keyPath:animation};
    }
}

就是这样!无需应用 animation.duration,因为 table 视图的 -beginUpdates-endUpdates 覆盖了它。一般来说,如果您将此技巧用作应用动画的无障碍方式,您将需要添加一个 animation.duration 并且可能还需要一个 animation.timingFunction.

我不使用自动布局,而是从 UITableViewCell 派生 class 并使用其 "layoutSubviews" 以编程方式设置子视图的框架。所以我尝试修改 cnotethegr8 的答案以满足我的需要,然后我想出了以下内容。

重新实现单元格的"setFrame",将单元格的内容高度设置为单元格高度并触发动画块中子视图的重新布局:

@interface MyTableViewCell : UITableViewCell
...
@end

@implementation MyTableViewCell

...

- (void) setFrame:(CGRect)frame
{
    [super setFrame:frame];

    if (self.window)
    {
        [UIView animateWithDuration:0.3 animations:^{
            self.contentView.frame = CGRectMake(
                                        self.contentView.frame.origin.x,
                                        self.contentView.frame.origin.y,
                                        self.contentView.frame.size.width,
                                        frame.size.height);
            [self setNeedsLayout];
            [self layoutIfNeeded];
        }];
    }
}

...

@end

成功了:)

iOS 10 in SwiftAutoLayout 的解决方案:

将此代码放入您的自定义 UITableViewCell

override func layoutSubviews() {
    super.layoutSubviews()

    if #available(iOS 10, *) {
        UIView.animateWithDuration(0.3) { self.contentView.layoutIfNeeded() }
    }
}

在Objective-C中:

- (void)layoutSubviews
{
    [super layoutSubviews];

    NSOperatingSystemVersion ios10 = (NSOperatingSystemVersion){10, 0, 0};
    if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:ios10]) {
        [UIView animateWithDuration:0.3
                         animations:^{
                             [self.contentView layoutIfNeeded];
                         }];
    }
}

如果您像下面这样配置了 UITableViewDelegate,这将动画 UITableViewCell 改变高度:

func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    return selectedIndexes.contains(indexPath.row) ? Constants.expandedTableViewCellHeight : Constants.collapsedTableViewCellHeight
}

func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
    tableView.deselectRowAtIndexPath(indexPath, animated: true)
    tableView.beginUpdates()
    if let index = selectedIndexes.indexOf(indexPath.row) {
        selectedIndexes.removeAtIndex(index)
    } else {
        selectedIndexes.append(indexPath.row)
    }
    tableView.endUpdates()
}

在 iOS 10 上遇到同样的问题(在 iOS 9 上一切正常),解决方案是通过 UITableViewDelegate 提供实际的 estimatedRowHeight 和 heightForRow方法实施。

Cell的subview是用AutoLayout定位的,所以我用UITableViewAutomaticDimension来计算高度(你可以试试设置成tableView的rowHeight 属性).

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {

    CGFloat result = kDefaultTableViewRowHeight;

    if (isAnimatingHeightCellIndexPAth) {
        result = [self precalculatedEstimatedHeight];
    }

    return result;
 }


- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return UITableViewAutomaticDimension;
}

所以对于使用布局约束的 UITableViewCell 和使用自动单元格高度 (UITableViewAutomaticDimension) 的 UITableView,我遇到了这个问题。所以我不确定这个解决方案有多少对你有用,但我把它放在这里是因为它密切相关。

主要实现是降低导致高度变化发生的约束的优先级:

self.collapsedConstraint = self.expandingContainer.topAnchor.constraintEqualToAnchor(self.bottomAnchor).priority(UILayoutPriorityDefaultLow)
self.expandedConstraint = self.expandingContainer.bottomAnchor.constraintEqualToAnchor(self.bottomAnchor).priority(UILayoutPriorityDefaultLow)

self.expandedConstraint?.active = self.expanded
self.collapsedConstraint?.active = !self.expanded

其次,我制作 tableview 动画的方式非常具体:

UIView.animateWithDuration(0.3) {

    let tableViewCell = tableView.cellForRowAtIndexPath(viewModel.index) as! MyTableViewCell
    tableViewCell.toggleExpandedConstraints()
    tableView.beginUpdates()
    tableView.endUpdates()
    tableViewCell.layoutIfNeeded()
}

请注意 layoutIfNeeded() 必须在 beginUpdates/endUpdates

之后出现

我认为这最后一部分可能对您有所帮助....

由于声誉原因无法发表评论,但 sam 的解决方案在 IOS 10 对我有用。

 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {

    EWGDownloadTableViewCell *cell = (EWGDownloadTableViewCell *)[tableView cellForRowAtIndexPath:indexPath];

    if (self.expandedRow == indexPath.row) {
        self.expandedRow = -1;
        [UIView animateWithDuration:0.3 animations:^{
            cell.constraintToAnimate.constant = 51;
            [tableView beginUpdates];
            [tableView endUpdates];
            [cell layoutIfNeeded];
        } completion:nil];
    } else {
        self.expandedRow = indexPath.row;
        [UIView animateWithDuration:0.3 animations:^{
            cell.constraintToAnimate.constant = 100;
            [tableView beginUpdates];
            [tableView endUpdates];
            [cell layoutIfNeeded];
        } completion:nil];
    } }

其中 constraintToAnimate 是添加到单元格 contentView 的子视图的高度约束。

Swift 4^

当我位于 table 底部并尝试在我的 table 视图中展开和折叠一些行时出现跳转问题。我首先尝试了这个解决方案:

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
}

但是,当我尝试展开该行,向上滚动,然后突然折叠该行时,问题又出现了。所以我尝试了这个:

let savedContentOffset = contentOffset

tableView.beginUpdates()
tableView.endUpdates()
tableView.layer.removeAllAnimations()

tableView.setContentOffset(savedContentOffset, animated: false)

这以某种方式解决了它。尝试这两种解决方案,看看哪种适合您。希望这有帮助。