web-dev-qa-db-ja.com

iOS 10のUITableViewCellアニメーションの高さの問題

私のUITableViewCellは、タップを認識すると高さをアニメーション化します。 iOS 9以下では、このアニメーションはスムーズで問題なく動作します。 iOS 10ベータでは、アニメーション中に不快なジャンプがあります。これを修正する方法はありますか?

これがコードの基本的な例です。

- (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ベースのアニメーションは機能しません。

たとえば、セルの下部に制限されたビューがある場合、それはジャンプします。解決策は、動的定数を使用して上部を拘束することです。または、下部に関連するビュー以外のビューを配置する別の方法を考えてください。

24
cnotethegr8

より良い解決策:

問題は、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にドロップすることです。これは、CALayerにリンクされていないUIViewを使用するのと同じように、高さの変化を検出して自動的にアニメーション化します。

最初のステップは、レイヤーが変更を検出したときに何が起こるかを調整することです。このコードは、ビューのサブクラスにある必要があります。そのビューはtableViewCell.contentViewのサブビューである必要があります。コードでは、ビューのレイヤーのアクションプロパティにアニメーションのキーがあるかどうかを確認します。ない場合は、superを呼び出します。

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

次に、actionsプロパティにアニメーションを追加します。このコードは、ビューがウィンドウ上に配置された後に最適に適用される場合があります。事前に適用すると、ビューがウィンドウに移動するときにアニメーションが発生する場合があります。

- (void)didMoveToWindow {
    [super didMoveToWindow];

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

以上です!テーブルビューのanimation.durationおよび-beginUpdatesがオーバーライドするため、-endUpdatesを適用する必要はありません。通常、アニメーションを適用する手間のかからない方法としてこのトリックを使用する場合は、animation.durationと、場合によってはanimation.timingFunctionも追加する必要があります。

8
cnotethegr8

Swiftを含むAutoLayoutiOS 10の解決策:

このコードをカスタム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];
                         }];
    }
}

以下のようにUITableViewCellを設定した場合、これはUITableViewDelegateをアニメーション化して高さを変更します。

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()
}
12
GiomGiom

自動レイアウトを使用せず、UITableViewCellからクラスを派生させ、その「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

それはトリックをしました:)

6
Markus

したがって、レイアウトの制約を使用するUITableViewCellと自動セルの高さ(UITableView)を使用するUITableViewAutomaticDimensionでこの問題が発生しました。したがって、このソリューションがどれだけ効果があるかはわかりませんが、密接に関連しているため、ここに配置します。

主な実現は、高さの変化を引き起こす制約の優先度を下げることでした:

_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
_

次に、テーブルビューをアニメーション化する方法は非常に具体的でした。

_UIView.animateWithDuration(0.3) {

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

layoutIfNeeded()beginUpdates/endUpdatesの後に来る必要があることに注意してください。

この最後の部分が参考になると思います。

1
Sam

Swift 4 ^

テーブルの最下部にいて、tableViewの一部の行を展開したり折りたたんだりしようとしたときにジャンプの問題がありました。私は最初にこの解決策を試しました:

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)

これはどういうわけかそれを修正します。両方の解決策を試して、何が効果的かを確認してください。お役に立てれば。

1
arvinq

IOS 10で同じ問題に直面しました(iOS 9ではすべて問題ありませんでした)、解決策は、UITableViewDelegateメソッドの実装を介して実際のestimatedRowHeightおよびheightForRowを提供することでした。

セルのサブビューは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;
}
0

評判のためコメントはできませんが、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に追加されたサブビューの高さの制約です。

0
Ewout Gelling