My UITableViewCell
will animate it's height when recognizing a tap. In iOS 9 and below this animation is smooth and works without issues. In iOS 10 beta there's a jarring jump during the animation. Is there a way to fix this?
Here is a basic example of the code.
- (void)cellTapped {
[self.tableView beginUpdates];
[self.tableView endUpdates];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return self.shouldExpandCell ? 200.0f : 100.0f;
}
EDIT: 9/8/16
The issue still exists in the GM. After more debugging I have discovered the issue is related to the cell immediately jumping to the new height and then will animate the relevant cells offset. This means any CGRect
based animation which is dependent on the cells bottom will not work.
For instance if I have a view constrained to the cells bottom, it will jump. The solution would involve a constraint to the top with a dynamic constant. Or think of another way to position your views other then related to the bottom.
The solution for iOS 10
in Swift
with AutoLayout
:
Put this code in your custom UITableViewCell
override func layoutSubviews() {
super.layoutSubviews()
if #available(iOS 10, *) {
UIView.animateWithDuration(0.3) { self.contentView.layoutIfNeeded() }
}
}
In 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];
}];
}
}
This will animate UITableViewCell
change height if you have configured your UITableViewDelegate
like below :
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()
}
Better Solution:
The issue is when the UITableView
changes the height of a cell, most likely from -beginUpdates
and -endUpdates
. Prior to iOS 10 an animation on the cell would take place for both the size
and the origin
. Now, in iOS 10 GM, the cell will immediately change to the new height and then will animate to the correct offset.
The solution is pretty simple with constraints. Create a guide constraint which will update it's height and have the other views which need to be constrained to the bottom of the cell, now constrained to this 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;
}
Then update the guides height when needed.
- (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;
}
}
Note that putting the update guide in -setFrame:
might not be the best place. As of now I have only built this demo code to create a solution. Once I finish updating my code with the final solution, if I find a better place to put it I will edit.
Original Answer:
With the iOS 10 beta nearing completion, hopefully this issue will be resolved in the next release. There's also an open bug report.
My solution involves dropping to the layer's CAAnimation
. This will detect the change in height and automatically animate, just like using a CALayer
unlinked to a UIView
.
The first step is to adjust what happens when the layer detects a change. This code has to be in the subclass of your view. That view has to be a subview of your tableViewCell.contentView
. In the code, we check if the view's layer's actions property has the key of our animation. If not just call super.
- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
return [layer.actions objectForKey:event] ?: [super actionForLayer:layer forKey:event];
}
Next you want to add the animation to the actions property. You might find this code is best applied after the view is on the window and laid out. Applying it beforehand might lead to an animation as the view moves to the window.
- (void)didMoveToWindow {
[super didMoveToWindow];
if (self.window) {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"bounds"];
self.layer.actions = @{animation.keyPath:animation};
}
}
And that's it! No need to apply an animation.duration
since the table view's -beginUpdates
and -endUpdates
overrides it. In general if you use this trick as a hassle-free way of applying animations, you will want to add an animation.duration
and maybe also an animation.timingFunction
.
I don't use auto layout, instead I derive a class from UITableViewCell and use its "layoutSubviews" to set the frames of the subviews programmatically. So I tried to modify cnotethegr8's answer to fit my needs, and I came up with the following.
Reimplement "setFrame" of the cell, set the cell's content height to the cell height and trigger a relayout of the subviews in an animation block:
@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
That did the trick :)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With