web-dev-qa-db-fra.com

Problème de hauteur d'animation UITableViewCell dans iOS 10

Ma UITableViewCell animera sa hauteur lors de la reconnaissance d'un tap. Sous iOS 9 et inférieur, cette animation est fluide et fonctionne sans problème. Dans iOS 10 bêta, il y a un saut saccadé pendant l'animation. Y'a t'il un moyen d'arranger cela?

Voici un exemple de base du code.

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

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

EDIT: 08/09/16

Le problème existe toujours dans le GM. Après plus de débogage, j'ai découvert que le problème était lié au fait que la cellule sautait immédiatement à la nouvelle hauteur et animerait ensuite le décalage des cellules correspondantes. Cela signifie que toute animation basée sur CGRect qui dépend du bas des cellules ne fonctionnera pas. 

Par exemple, si j'ai une vue limitée au bas des cellules, elle sautera. La solution impliquerait une contrainte au sommet avec une constante dynamique. Ou pensez à une autre façon de positionner vos points de vue par rapport au fond.

22
cnotethegr8

Meilleure solution:

Le problème se pose lorsque UITableView modifie la hauteur d'une cellule, probablement entre -beginUpdates et -endUpdates. Avant iOS 10, une animation avait lieu sur la cellule pour size et Origin. Maintenant, dans iOS 10 GM, la cellule passera immédiatement à la nouvelle hauteur, puis s'animera au décalage correct.

La solution est assez simple avec des contraintes. Créez une contrainte de guide qui mettra à jour sa hauteur et les autres vues qui doivent être contraintes au bas de la cellule, maintenant contraintes à ce guide.

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

Puis mettez à jour la hauteur des guides si nécessaire.

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

Notez que placer le guide de mise à jour dans -setFrame: n'est peut-être pas le meilleur endroit. Pour l'instant, j'ai seulement construit ce code de démonstration pour créer une solution. Une fois que j'ai fini de mettre à jour mon code avec la solution finale, si je trouve un meilleur endroit pour le mettre, je le modifierai.

Réponse originale:

La version bêta d'iOS 10 étant sur le point d'être terminée, nous espérons que ce problème sera résolu dans la prochaine version. Il y a aussi un rapport de bogue ouvert.

Ma solution consiste à passer à la couche CAAnimation. Ceci détectera le changement de hauteur et s'animera automatiquement, exactement comme si vous utilisiez un CALayer non lié à un UIView.

La première étape consiste à ajuster ce qui se passe lorsque la couche détecte un changement. Ce code doit être dans la sous-classe de votre vue. Cette vue doit être une sous-vue de votre tableViewCell.contentView. Dans le code, nous vérifions si la propriété actions de la couche de la vue possède la clé de notre animation. Si ce n'est pas juste appeler super.

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

Ensuite, vous souhaitez ajouter l'animation à la propriété actions. Vous constaterez peut-être que ce code est mieux appliqué une fois que la vue est affichée et présentée. Si vous l'appliquez au préalable, une animation risque de se produire lorsque la vue passe à la fenêtre.

- (void)didMoveToWindow {
    [super didMoveToWindow];

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

Et c'est tout! Pas besoin d'appliquer un animation.duration puisque les -beginUpdates et -endUpdates de la vue de table le remplacent. En général, si vous utilisez cette astuce pour appliquer des animations sans problème, vous voudrez ajouter un animation.duration et peut-être aussi un animation.timingFunction.

8
cnotethegr8

La solution pour iOS 10 dans Swift avec AutoLayout:

Mettez ce code dans votre UITableViewCell personnalisé

override func layoutSubviews() {
    super.layoutSubviews()

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

En 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];
                         }];
    }
}

Ceci animera UITableViewCell changer la hauteur si vous avez configuré votre UITableViewDelegate comme ci-dessous:

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

Je n'utilise pas la mise en page automatique, je dérive une classe de UITableViewCell et utilise ses "layoutSubviews" pour définir les cadres des sous-vues par programme. J'ai donc essayé de modifier la réponse de cnotethegr8 pour l'adapter à mes besoins, et j'ai proposé ce qui suit.

Réimplémentez "setFrame" de la cellule, définissez la hauteur du contenu de la cellule sur la hauteur de la cellule et déclenchez un relais des sous-vues dans un bloc d'animation: 

@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

Cela a fait le tour :)

6
Markus

J'ai donc eu ce problème pour une UITableViewCell qui utilisait des contraintes de présentation et une UITableView qui utilisait des hauteurs de cellule automatiques (UITableViewAutomaticDimension). Donc, je ne sais pas dans quelle mesure cette solution fonctionnera pour vous, mais je la pose ici car elle est étroitement liée.

La principale réalisation consistait à réduire la priorité des contraintes à l’origine du changement de hauteur:

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

Deuxièmement, la manière dont j'anime la table est très spécifique:

UIView.animateWithDuration(0.3) {

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

Notez que la layoutIfNeeded() devait arriver APRES beginUpdates/endUpdates

Je pense que cette dernière partie pourrait vous être utile ....

1
Sam

Face au même problème sur iOS 10 (tout allait bien sur iOS 9), la solution consistait à fournir la valeur réelle de ratedRowHeight et heightForRow via la mise en œuvre de méthodes UITableViewDelegate. 

Les sous-vues de cellules positionnées avec AutoLayout, j'ai donc utilisé UITableViewAutomaticDimension pour le calcul de la hauteur (vous pouvez essayer de le définir comme propriété rowHeight de tableView).

- (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
Alexander Larionov

Swift 4 ^

Avait le problème de saut quand j'étais au bas de la table et a essayé de développer et de réduire certaines lignes de mon tableView. J'ai d'abord essayé cette solution: 

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
}

Mais ensuite, le problème est réapparu lorsque j’essayais d’agrandir la ligne, de faire défiler la liste vers le haut, puis de la réduire soudainement. Alors j'ai essayé ceci:

let savedContentOffset = contentOffset

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

tableView.setContentOffset(savedContentOffset, animated: false)

Ceci en quelque sorte le réparer. Essayez les deux solutions et voyez ce qui fonctionne pour vous. J'espère que cela t'aides.

0
arvnq

Je ne peux pas commenter pour des raisons de réputation, mais la solution de sam a fonctionné pour moi sur 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];
    } }

où constraintToAnimate est une contrainte de hauteur sur une sous-vue ajoutée aux cellules contentView.

0
Ewout Gelling