UITableViewCell увеличивает размер высоты в iOS 10

My 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;
}

РЕДАКТИРОВАТЬ: 9/8/16

Проблема все еще существует в GM. После большей отладки я обнаружил, что проблема связана с тем, что ячейка сразу перескакивает на новую высоту и затем оживляет смещение соответствующих ячеек. Это означает, что любая анимация на основе CGRect, которая зависит от дна ячеек, не будет работать.

Например, если у меня есть представление, ограниченное дном ячеек, оно будет прыгать. Решение будет связано с ограничением сверху с динамической константой. Или подумайте о другом способе позиционирования ваших взглядов, а затем связанных с нижней.

Ответ 1

Лучшее решение:

Проблема заключается в том, что UITableView изменяет высоту ячейки, скорее всего, от -beginUpdates и -endUpdates. До iOS 10 анимация в ячейке будет иметь место как для size, так и для origin. Теперь, в 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. В коде мы проверяем, имеет ли свойство действия слоя представления ключ нашей анимации. Если не просто вызов супер.

- (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.

Ответ 2

Решение для iOS 10 в Swift с автоматической компоновкой:

Поместите этот код в свой пользовательский 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()
}

Ответ 3

Я не использую автоматический макет, вместо этого я получаю класс из 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

Это сделал трюк:)

Ответ 4

Столкнувшись с той же проблемой на iOS 10 (все было в порядке на iOS 9), решение заключалось в предоставлении фактического оценочногоRowHeight и heightForRow с помощью реализации методов UITableViewDelegate.

Поля ячейки, позиционированные с помощью 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;
}

Ответ 5

Итак, у меня была эта проблема для 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

Я думаю, что эта последняя часть может быть вам полезной....

Ответ 6

Невозможно прокомментировать из-за репутации, но решение 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.