Animate UITableView index, a la Apple Music? - ios

Simple question:
Apple Music's table views only show an A-Z section index on the right edge once they've been scrolled down past a certain threshold, and that index animates in and out nicely.
I've been able to trigger the appearing / disappearing behaviour with the code below, but the index just pops in and out, there's no animation, and I can't find any way to get one to show up.
func sectionIndexTitlesForTableView(tableView: UITableView) -> [AnyObject]! {
if tableView.contentOffset.y > 88 {
return DataManager.sharedManager.frc!.sectionIndexTitles
} else {
return []
}
}
func scrollViewDidScroll(scrollView: UIScrollView) {
tableView.reloadSectionIndexTitles()
}
This basically means each time the scroll view ticks, it'll reload the section indexes, then conditionally hide or show the index based on the offset of the table. As I say, it works, but it doesn't animate the index, and I'd really love that functionality if possible.

Seems like it is not possible to do that, ate least apple doesn't provide any API to animate the section index view. I am able to slide in/out the indexes, but it doesn't resize the cells' contentView.
When you animate the section index view, right after the animation it comes back to its initial position. So I am basically setting the color to clearColor/black when hidden/visible.
I am not sure if apple approves this code since it's kind of using undocumented APIs
- (UIView *)indexTitlesView {
NSArray *subViews = [self.tableView subviews];
for (UIView *view in subViews) {
if ([NSStringFromClass([view class]) isEqualToString:#"UITableViewIndex"]) {
return view;
}
}
return nil;
}
- (void)slideIndexTitlesViewIn {
UIView *indexTitlesView = [self indexTitlesView];
CABasicAnimation *contentPositionAnimation = [CABasicAnimation animationWithKeyPath:#"position.x"];
contentPositionAnimation.fromValue = #(CGRectGetWidth(indexTitlesView.frame));
contentPositionAnimation.toValue = #(0);
contentPositionAnimation.additive = YES;
contentPositionAnimation.duration = 0.3;
contentPositionAnimation.delegate = self;
contentPositionAnimation.removedOnCompletion = NO;
contentPositionAnimation.fillMode = kCAFillModeForwards;
[indexTitlesView.layer removeAllAnimations];
[indexTitlesView.layer addAnimation:contentPositionAnimation forKey:#"slideInAnimation"];
self.tableView.sectionIndexColor = [UIColor blackColor];
}
- (void)slideIndexTitlesViewOut {
UIView *indexTitlesView = [self indexTitlesView];
CABasicAnimation *contentPositionAnimation = [CABasicAnimation animationWithKeyPath:#"position.x"];
contentPositionAnimation.fromValue = #(0);
contentPositionAnimation.toValue = #(CGRectGetWidth(indexTitlesView.frame));
contentPositionAnimation.additive = YES;
contentPositionAnimation.duration = 0.3;
contentPositionAnimation.delegate = self;
contentPositionAnimation.removedOnCompletion = NO;
contentPositionAnimation.fillMode = kCAFillModeForwards;
[indexTitlesView.layer removeAllAnimations];
[indexTitlesView.layer addAnimation:contentPositionAnimation forKey:#"slideOutAnimation"];
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
UIView *indexTitlesView = [self indexTitlesView];
NSArray *keys = [indexTitlesView.layer animationKeys];
for (NSString *key in keys) {
if ([indexTitlesView.layer animationForKey:key] == anim) {
if ([key isEqualToString:#"slideOutAnimation"] && flag == YES) {
self.tableView.sectionIndexColor = [UIColor clearColor];
}
[indexTitlesView.layer removeAllAnimations];
return;
}
}
}

Just use the animateWithDuration() method set the alpha value to zero and then animate it in by setting the value to 1

I would not suggest playing with private classes, since it can easily break in a future OS release.
I've implemented a custom control for the exact purpose. It mimics the native table index appearance while providing much more customization capabilities.
Check https://github.com/mindz-eye/MYTableViewIndex for details.

Related

How to manage CALayer animations throughout a hierarchy

This is a follow-up question to How to synchronize CALayer and UIView animations up and down a complex hierarchy
Lets say I have a composite layer (Top) that is a subclass of CALayer and has any number of children. Top has 2 child layers within it. The first sublayer (A) should always be a fixed width - lets say 100 pixels wide. The second sublayer (B) should be the remainder of the size of Top. Both A and B should occupy the entire height of Top. This is pretty straightforward to code up in layoutSubviews.
Let's presume that Top has no knowledge of A or B. Also presume that Top has a delegate that controls when it should be animated (the delegate provides actionForLayer:forKey: and no other CALayer delegate functions).
I'd like to devise a strategy where for every possible size of Top, the user will always see A and B rendered according to the constraints listed above - even when the size of Top is being animated, even when it is being animated with any variety of animation parameters (durations, functions, offsets, etc).
Just as Top's animations are driven from some containing view or layer through its delegate - it seems that A and B should have their animations setup their containing layer - Top. I want to keep things well-composed, so I don't want the layout of A & B within Top to need to be understood by anything other than Top.
So - the question is what's the best strategy to chain the animations down the layer tree to keep all of the animation parameters in sync?
Here's some sample code that does chaining through the use of actionForLayer:forKey:, but middle function has to go through some fairly involved work (which isn't included) to translate all of the settings from its animation to the sublayer's animation. Not included in this sample is any code that deals with interpolating the values of the bounds. For example, imagine a case where an animation is setup to use a different fromValue, or a keyframe animation. Those values would need to be solved for the sublayers and applied accordingly.
#import "ViewController.h"
#interface MyTopLayer : CALayer
#end
static const CGFloat fixedWidth = 100.0;
#implementation MyTopLayer
-(instancetype)init {
self = [super init];
if (self) {
self.backgroundColor = [[UIColor redColor] CGColor];
CALayer *fixedLayer = [[CALayer alloc] init];
CALayer *slackLayer = [[CALayer alloc] init];
[self addSublayer:fixedLayer];
[self addSublayer:slackLayer];
fixedLayer.anchorPoint = CGPointMake(0,0);
fixedLayer.position = CGPointMake(0,0);
slackLayer.anchorPoint = CGPointMake(0,0);
slackLayer.position = CGPointMake(fixedWidth,0);
fixedLayer.backgroundColor = [[UIColor yellowColor] CGColor];
slackLayer.backgroundColor = [[UIColor purpleColor] CGColor];
//fixedLayer.delegate = self; // no reason to ever animate this layer since it is static
slackLayer.delegate = self;
}
return self;
}
-(id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
if (![event isEqualToString:#"bounds"]) {
return nil;
}
CAAnimation *boundsAnim = [self animationForKey:#"bounds"];
NSLog(#"boundsAnim=%#", boundsAnim);
if (!boundsAnim) {
return (id<CAAction>)[NSNull null];
}
CAAnimation *sublayerBoundsAnim;
if ([boundsAnim isKindOfClass:[CABasicAnimation class]]) {
CABasicAnimation *subAnim = [CABasicAnimation animationWithKeyPath:#"bounds"];
// transform properties, like from, to & by value from boundsAnim (outer) to the inner layer's animation
sublayerBoundsAnim = subAnim;
} else {
CAKeyframeAnimation *subAnim = [CAKeyframeAnimation animationWithKeyPath:#"bounds"];
// copy/interpolate keyframes
sublayerBoundsAnim = subAnim;
}
sublayerBoundsAnim.timeOffset = boundsAnim.timeOffset;
sublayerBoundsAnim.duration = boundsAnim.duration;
sublayerBoundsAnim.timingFunction = boundsAnim.timingFunction;
return sublayerBoundsAnim;
}
-(void)layoutSublayers {
{
CALayer *fixedLayer = [self.sublayers firstObject];
CGRect b = self.bounds;
b.size.width = fixedWidth;
fixedLayer.bounds = b;
}
{
CALayer *slackLayer = [self.sublayers lastObject];
CGRect b = self.bounds;
b.size.width -= fixedWidth;
slackLayer.bounds = b;
}
}
#end
#interface MyView : UIView
#end
#implementation MyView
{
bool _shouldAnimate;
}
+(Class)layerClass {
return [MyTopLayer class];
}
-(instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.layer.delegate = self;
UITapGestureRecognizer *doubleTapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(doubleTapRecognizer:)];
doubleTapRecognizer.numberOfTapsRequired = 2;
[self addGestureRecognizer:doubleTapRecognizer];
UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
action:#selector(tapRecognizer:)];
[tapRecognizer requireGestureRecognizerToFail:doubleTapRecognizer];
[self addGestureRecognizer:tapRecognizer];
}
return self;
}
CGFloat getRandWidth() {
const static int maxWidth=1024;
const static int minWidth=fixedWidth*1.1;
return minWidth+((((CGFloat)rand())/(CGFloat)RAND_MAX)*(maxWidth-minWidth));
}
-(void)tapRecognizer:(UITapGestureRecognizer*) gr {
_shouldAnimate = true;
CGFloat w = getRandWidth();
self.layer.bounds = CGRectMake(0,0,w,self.layer.bounds.size.height);
}
-(void)doubleTapRecognizer:(UITapGestureRecognizer*) gr {
_shouldAnimate = false;
CGFloat w = getRandWidth();
self.layer.bounds = CGRectMake(0,0,w,self.layer.bounds.size.height);
}
-(id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {
if (_shouldAnimate) {
if ([event isEqualToString:#"bounds"]) {
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:event];
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
anim.duration = 2.0;
//anim.timeOffset = 0.5;
anim.fromValue = [NSValue valueWithCGRect:CGRectMake(0,0,100,100)];
return anim;
} else {
return nil;
}
} else {
return (id<CAAction>)[NSNull null];
}
}
#end
My question is - does anybody have a better way to get this done? It seems a little bit scary that I've not seen any mention of this sort of hierarchical chaining anywhere. I'm aware that I would probably also need to do some more work on canceling sublayer animations when the top layer's animation is canceled. Relying simply on the currently attached animation, especially w/out concern for the current time that that function is in seems like it could be a source of errors somewhere down the line.
I'm also not sure how well this would perform in the wild since they aren't in the same animation group. Any thoughts there would be greatly appreciated.

Objective C animates wrong button when scrolling in tablview

- (IBAction)playShout:(UIButton *)sender {
self.tableView.userInteractionEnabled=NO;
CGPoint point = [sender.superview convertPoint:sender.frame.origin toView:self.tableView];
NSIndexPath *indexPath = self.tempIndexPath;//[self.tableView indexPathForRowAtPoint:point];
DisplayPersonTableViewCell * cell=[self.tableView cellForRowAtIndexPath:indexPath];
UIButton * button=cell.playButton;
self.tableView.userInteractionEnabled=YES;
PFObject * shoutToPlay=[self.shoutsArrayForUserBeingDisplayed objectAtIndex:indexPath.row];
[shoutToPlay fetchIfNeeded];
[AnimationViewController endCircleAnimation];
NSData * audioData=[self.audioDataDict objectForKey:shoutToPlay.objectId];
if (player.playing) {
if(player.data==audioData) {
[player stop];
[AnimationViewController endCircleAnimation];
} else{
[self setAllTableViewCellsToPlayButton];
[AnimationViewController endCircleAnimation];
sender.layer.cornerRadius=sender.frame.size.width/2;
player = [[AVAudioPlayer alloc] initWithData:audioData error:nil];
player.delegate=self;
[player prepareToPlay];
if (DISPLAY_IS_MUTED) {
player.volume=0;
}
[player play];
[AnimationViewController animateButton:cell.playButton ForDuration:player.duration withColor:[UIColor whiteColor]];
}
} else {
sender.layer.cornerRadius=sender.frame.size.width/2;
player = [[AVAudioPlayer alloc] initWithData:audioData error:nil];
player.delegate=self;
[player prepareToPlay];
if (DISPLAY_IS_MUTED) {
player.volume=0;
}
[player play];
[AnimationViewController animateButton:sender ForDuration:player.duration withColor:[UIColor whiteColor]];
}
}
Very simple and very easy, of course. But if I perform an animation on a button in that cell, and then scroll the tableview, it will switch and start animating the wrong cells. It's as if the fact that the superview's frame is changing affect's which button is being animated.
Any advice?
Animation method:
+(void)animateButton:(UIButton *)button ForDuration:(NSTimeInterval)duration withColor:(UIColor *)color {
circle = [CAShapeLayer layer];
circle.fillColor = nil;
circle.lineWidth = button.frame.size.height/2+2;
circle.strokeColor = color.CGColor;
circle.bounds = CGRectMake(0, 0, button.frame.size.width/2, button.frame.size.height/2);
circle.frame= CGRectMake(button.frame.size.width/4, button.frame.size.height/4, button.frame.size.width/2+2, button.frame.size.height/2+2);
circle.path = [UIBezierPath bezierPathWithOvalInRect:circle.bounds].CGPath;
[button.layer addSublayer:circle];
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = duration;
drawAnimation.repeatCount = 1.0;
drawAnimation.removedOnCompletion = YES;
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:1.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[circle addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
}
Are you implementing the prepareForReuse method in your cell? There you must stop running animations and reset the state of the cell for the reuse.
From the docs:
prepareForReuse
Prepares a reusable cell for reuse by the table view's delegate.
If a UITableViewCell object is reusable—that is, it has a reuse identifier—this method is invoked just before the object is returned
from the UITableView method dequeueReusableCellWithIdentifier:. For
performance reasons, you should only reset attributes of the cell that
are not related to content, for example, alpha, editing, and selection
state. The table view's delegate in tableView:cellForRowAtIndexPath:
should always reset all content when reusing a cell. If the cell
object does not have an associated reuse identifier, this method is
not called. If you override this method, you must be sure to invoke
the superclass implementation.
You need to implement a custom UITableViewCell (create a subclass of it) and implement the prepareForReuse method.
There you can write the logic to stop the animation.

UIKit Dynamics: Attachment inside UITableViewCell

My table view cells contain a circle in an UIView, indicating a value. I want to add the UIKit Dynamics attachment behaviour to that circle in order to for it to lag a bit when scrolling.
I don't want to attach the individual cells to each other but only the circle view to the UITableViewCell. The rest of the cell should scroll as usual.
Problem: The UITableViewCell has its origin always at (0, 0). How can I add the circle to a view that actually does move when scrolling?
I finally got it to work. The UITableView moves the coordinate system of every cell and of all views contained within that cell. Therefor I needed to manually move my view inside the UITableViewCell during scrolling while still referring to the initial anchor point.
The table view controller:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
BOOL scrollingUp = '\0';
if (self.lastContentOffset > scrollView.contentOffset.y) {
scrollingUp = YES;
}
else if (self.lastContentOffset < scrollView.contentOffset.y) {
scrollingUp = NO;
}
NSInteger offset = 64; // To compensate for the navigation bar.
if (scrollingUp) {
offset = offset - scrollView.contentOffset.y;
}
else {
offset = offset + scrollView.contentOffset.y;
}
// Limit the offset so the views will not disappear during fast scrolling.
if (offset > 10) {
offset = 10;
}
else if (offset < -10) {
offset = -10;
}
// lastContentOffset is an instance variable.
self.lastContentOffset = scrollView.contentOffset.y;
for (UITableViewCell *cell in self.tableView.visibleCells) {
// Use CoreAnimation to prohibit flicker.
[UIView beginAnimations:#"Display notification" context:nil];
[UIView setAnimationDuration:0.5f];
[UIView setAnimationBeginsFromCurrentState:YES];
cell.view.frame = CGRectMake(cell.view.frame.origin.x, offset, cell.view.frame.size.width, cell.view.frame.size.height);
[UIView commitAnimations];
[cell.dynamicAnimator updateItemUsingCurrentState:cell.view];
}
}
The table view cell:
-(void)layoutSubviews {
[super layoutSubviews];
// _view is the animated UIView.
UIDynamicItemBehavior *viewBehavior = [[UIDynamicItemBehavior alloc] initWithItems:#[_view]];
viewBehavior.elasticity = 0.9f;
UIAttachmentBehavior *attachmentBehaviorView = [[UIAttachmentBehavior alloc] initWithItem:_view attachedToAnchor:CGPointMake(_anchorView.frame.origin.x + _anchorView.frame.size.width / 2.0f, _anchorView.frame.origin.y + _anchorView.frame.size.height / 2.0f)];
attachmentBehaviorView.damping = 8.0f;
attachmentBehaviorView.frequency = 4.0f;
attachmentBehaviorView.length = 0.0f;
[_dynamicAnimator addBehavior:viewBehavior];
[_dynamicAnimator addBehavior:attachmentBehaviorView];
}
You can change the anchorPoint of UIAttachmentBehavior during -[scrollViewDidScroll:]. You may refer to the following code snippet:
- (void)viewDidLoad
{
UIDynamicAnimator *animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
UIAttachmentBehavior *behavior1 = [[UIAttachmentBehavior alloc] initWithItem:self.circleView
attachedToAnchor:[self tableViewAnchor]];
behavior1.length = 10.0;
behavior1.damping = 0.3;
behavior1.frequency = 2.5;
[animator addBehavior:behavior1];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
behavior1.anchorPoint = [self.tableView convertPoint:[self tableViewAnchor] toView:self.view];
}
- (CGPoint)tableViewAnchor
{
return CGPointMake(160.0, 154.0); // return your target coordination w.r.t. the table view
}
Preview:

UIview Flip darkens the Views

I've been implementing a simple FlipView in iOS : A UIView that contains two subviews, displaying one at a time, and when you click on it, it flips them.
I'm using the following to animate the flipping.
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
#synchronized(self){
if(!self.flipping){
self.flipping = YES;
UIView *toView = self.currentView == self.primaryView ? self.secondaryView : self.primaryView;
[UIView transitionFromView:self.currentView toView:toView duration:self.speed options:UIViewAnimationOptionTransitionFlipFromLeft|UIViewAnimationOptionCurveEaseInOut completion:^(BOOL finished) {
[self.currentView removeFromSuperview];
self.currentView = toView;
self.flipping = NO;
}];
}
}
}
Pretty straight forward, right ?
But what bugs me is that, while the views are flip, the flipped content is darkened. Which shows, against a light background.
Would anyone knows a solution to have the exact same animation, but without the darkening (<= is that even a word ?)
Thanks in advance !
PS : I'm targeting IOS 5 and above.
I recently had a problem with similar symptoms and I was adding a subview over and over again else where in my code whenever I committed a certain action. Maybe you are doing something similar? When your touches end, are you doing something else to your flipped content? You probably need to remove the subviews being added IF that is your problem.
I succeeded, getting inspiration in the code I found here http://www.mycodestudio.com/blog/2011/01/10/coreanimation/ (and he, himself, took inspiration from http://www.mentalfaculty.com/mentalfaculty/Blog/Entries/2010/9/22_FLIPPIN_OUT_AT_NSVIEW.html)
Anyway, what I do spin between two views.
- (void)flip{
#synchronized(self){
if(!self.flipping){
self.flipping = YES;
UIView *bottomView = self.currentView == self.primaryView ? self.secondaryView : self.primaryView;
CALayer *top = self.currentView.layer;
CALayer *bot = bottomView.layer;
CAAnimation *topAnimation = [self flipAnimationWithDuration:self.speed/2.0 forLayerBeginningOnTop:YES scaleFactor:1];
CAAnimation *bottomAnimation = [self flipAnimationWithDuration:self.speed/2.0 forLayerBeginningOnTop:NO scaleFactor:1];
CGFloat zDistance = 1500.0f;
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1. / zDistance;
top.transform = perspective;
bot.transform = perspective;
topAnimation.delegate = self;
[CATransaction setCompletionBlock:^{
[top removeAllAnimations];
[self.currentView removeFromSuperview];
self.currentView = bottomView;
[self addSubview:bottomView];
[CATransaction setCompletionBlock:^{
self.flipping = NO;
[bot removeAllAnimations];
}];
[CATransaction begin];
[bot addAnimation:bottomAnimation forKey:#"flip"];
[CATransaction commit];
}];
[CATransaction begin];
[top addAnimation:topAnimation forKey:#"flip"];
[CATransaction commit];
}
}
}
-(CAAnimation *)flipAnimationWithDuration:(NSTimeInterval)aDuration forLayerBeginningOnTop:(BOOL)beginsOnTop scaleFactor:(CGFloat)scaleFactor
{
// Rotating halfway (pi radians) around the Y axis gives the appearance of flipping
CABasicAnimation *flipAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
CGFloat startValue = beginsOnTop ? 0.0f : M_PI/2;
CGFloat endValue = beginsOnTop ? -M_PI/2 : 0.0f;
flipAnimation.fromValue = [NSNumber numberWithDouble:startValue];
flipAnimation.toValue = [NSNumber numberWithDouble:endValue];
// Shrinking the view makes it seem to move away from us, for a more natural effect
// Can also grow the view to make it move out of the screen
CABasicAnimation *shrinkAnimation = nil;
if (scaleFactor != 1.0 ) {
shrinkAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
shrinkAnimation.toValue = [NSNumber numberWithFloat:scaleFactor];
// We only have to animate the shrink in one direction, then use autoreverse to "grow"
shrinkAnimation.duration = aDuration * 0.5;
shrinkAnimation.autoreverses = YES;
}
// Combine the flipping and shrinking into one smooth animation
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
animationGroup.animations = [NSArray arrayWithObjects:flipAnimation, shrinkAnimation, nil];
// As the edge gets closer to us, it appears to move faster. Simulate this in 2D with an easing function
animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:beginsOnTop?kCAMediaTimingFunctionEaseIn:kCAMediaTimingFunctionEaseOut];
animationGroup.duration = aDuration;
// this really means keep the state of the object at whatever the anim ends at
// if you don't do this then it reverts back to the original state (e.g. brown layer)
animationGroup.fillMode = kCAFillModeForwards;
animationGroup.removedOnCompletion = NO;
return animationGroup;
}
The two views are named primaryView and secondaryView. You can use any view, (ImageView, text view...)

How do you set the duration for UICollectionView Animations?

I have a custom flow layout which is adjusting the attributes for cells when they are being inserted and deleted from the CollectionView with the following two functions, but I'm unable to figure out how you would adjust the default animation duration.
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath {
UICollectionViewLayoutAttributes* attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
// Assign the new layout attributes
attributes.transform3D = CATransform3DMakeScale(0.5, 0.5, 0.5);
attributes.alpha = 0;
return attributes;
}
- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)itemIndexPath {
UICollectionViewLayoutAttributes* attributes = [self layoutAttributesForItemAtIndexPath:itemIndexPath];
// Assign the new layout attributes
attributes.transform3D = CATransform3DMakeScale(0.5, 0.5, 0.5);
attributes.alpha = 0;
return attributes;
}
To solve problem without hack that was proposed in the answer by gavrix
you could subclass UICollectionViewLayoutAttributes with new property CABasicAnimation *transformAnimation, than create custom transformation with a suitable duration and assign it to attributes in initialLayoutAttributesForAppearingItemAtIndexPath, then in UICollectionViewCell apply the attributes as needed:
#interface AnimationCollectionViewLayoutAttributes : UICollectionViewLayoutAttributes
#property (nonatomic, strong) CABasicAnimation *transformAnimation;
#end
#implementation AnimationCollectionViewLayoutAttributes
- (id)copyWithZone:(NSZone *)zone
{
AnimationCollectionViewLayoutAttributes *attributes = [super copyWithZone:zone];
attributes.transformAnimation = _transformAnimation;
return attributes;
}
- (BOOL)isEqual:(id)other {
if (other == self) {
return YES;
}
if (!other || ![[other class] isEqual:[self class]]) {
return NO;
}
if ([(( AnimationCollectionViewLayoutAttributes *) other) transformAnimation] != [self transformAnimation]) {
return NO;
}
return YES;
}
#end
In Layout class
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath {
AnimationCollectionViewLayoutAttributes* attributes = (AnimationCollectionViewLayoutAttributes* )[super initialLayoutAttributesForAppearingItemAtIndexPath:itemIndexPath];
CABasicAnimation *transformAnimation = [CABasicAnimation animationWithKeyPath:#"transform"];
transformAnimation.duration = 1.0f;
CGFloat height = [self collectionViewContentSize].height;
transformAnimation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0, 2*height, height)];
transformAnimation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(0, attributes.bounds.origin.y, 0)];
transformAnimation.removedOnCompletion = NO;
transformAnimation.fillMode = kCAFillModeForwards;
attributes.transformAnimation = transformAnimation;
return attributes;
}
+ (Class)layoutAttributesClass {
return [AnimationCollectionViewLayoutAttributes class];
}
then in UICollectionViewCell apply the attributes
- (void) applyLayoutAttributes:(AnimationCollectionViewLayoutAttributes *)layoutAttributes
{
[[self layer] addAnimation:layoutAttributes.transformAnimation forKey:#"transform"];
}
change CALayer's speed
#implementation Cell
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.layer.speed =0.2;//default speed is 1
}
return self;
}
Building on #rotava's answer, you can temporarily set the animation speed by using a batch update of the collection view:
[self.collectionView performBatchUpdates:^{
[self.collectionView.viewForBaselineLayout.layer setSpeed:0.2];
[self.collectionView insertItemsAtIndexPaths: insertedIndexPaths];
} completion:^(BOOL finished) {
[self.collectionView.viewForBaselineLayout.layer setSpeed:1];
}];
After trying [CATransaction setAnimationDuration:] and [UIView setAnimationDuration:] in every possible phase of the layout process without success, I figured out a somewhat hacky way to change the duration of cell animations created by UICollectionView that doesn't rely on private API's.
You can use CALayer's speed property to change the relative media timing of animations performed on a given layer. For this to work with UICollectionView, you can change layer.speed to something less than 1 on the cell's layer. Obviously it's not great to have the cell's layer ALWAYS have a non-unity animation speed, so one option is to dispatch an NSNotification when preparing for cell animations, to which your cells subscribe, that will change the layer speed, and then change it back at an appropriate time after the animations are finished.
I don't recommend using this approach as a long-term solution as it's pretty roundabout, but it does work. Hopefully Apple will expose more options for UICollectionView animations in the future.
UICollectionView initiates all animations internally using some hardcoded value. However, you can always override that value until animations are committed.
In general, process looks like this:
begin animations
fetch all layout attribues
apply attributes to views (UICollectionViewCell's)
commit animations
applying attributes is done under each UICollectionViewCell and you can override animationDuration in appropriate method. The problem is that UICollectionViewCell has public method applyLayoutAttributes: BUT it's default implementation is empty!. Basically, UICollectionViewCell has other private method called _setLayoutAttributes: and this private method is called by UICollectionView and this private method calls applyLayoutAttributes: at the end. Default layout attributes, like frame, position, transform are applied with current animationDuration before applyLayoutAttributes: is called.
That said, you have to override animationDuration in private method _setLayoutAttributes:
- (void) _setLayoutAttributes:(PSTCollectionViewLayoutAttributes *)layoutAttributes
{
[UIView setAnimationDuration:3.0];
[super _setLayoutAttributes:layoutAttributes];
}
This is obviously, not applestore-safe. You can use one of those runtime hacks to override this private method safely.
You can set the layer's speed property (like in Rotoava's Answer) to change the control the speed of the animation. The problem is you are using arbitrary values because you do not know the actual duration of the insertion animation.
Using this post you can figure out what the default animation duration is.
newAnimationDuration = (1/layer.speed)*originalAnimationDuration
layer.speed = originalAnimationDuration/newAnimationDuration
If you wanted to make the animation 400ms long, in your layout you would:
- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes* attributes = [super finalLayoutAttributesForDisappearingItemAtIndexPath:indexPath];
//set attributes here
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
CGFloat originalAnimationDuration = [CATransaction animationDuration];
CGFloat newAnimationDuration = 0.4f;
cell.layer.speed = originalAnimationDuration/newAnimationDuration;
return attributes;
}
In my case I had cells which could be dragged off screen and I wanted to change the duration of the deletion animation based on the speed of the pan gesture.
In the gesture recognizer (which should be part of your collection view):
- (void)handlePanGesture:(UIPanGestureRecognizer *)sender
{
CGPoint dragVelocityVector = [sender velocityInView:self.collectionView];
CGFloat dragVelocity = sqrt(dragVelocityVector.x*dragVelocityVector.x + dragVelocityVector.y*dragVelocityVector.y);
switch (sender.state) {
...
case UIGestureRecognizerStateChanged:{
CustomLayoutClass *layout = (CustomLayoutClass *)self.collectionViewLayout;
layout.dragSpeed = fabs(dragVelocity);
...
}
...
}
Then in your customLayout:
- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)indexPath
{
UICollectionViewLayoutAttributes* attributes = [super finalLayoutAttributesForDisappearingItemAtIndexPath:indexPath];
CGFloat animationDistance = sqrt((x2-x1)*(x2-x1)+(y2-y1)*(y2-y1));
CGFloat originalAnimationDuration = [CATransaction animationDuration];
CGFloat newAnimationDuration = animationDistance/self.dragSpeed;
UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath];
cell.layer.speed = originalAnimationDuration/newAnimationDuration;
return attributes;
}
Without subclassing:
[UIView animateWithDuration:2.0 animations:^{
[self.collection reloadSections:indexSet];
}];
An update to #AshleyMills since forBaselineLayout is deprecated
This works
self.collectionView.performBatchUpdates({ () -> Void in
let indexSet = IndexSet(0...(numberOfSections - 1))
self.collectionView.insertSections(indexSet)
self.collectionView.forFirstBaselineLayout.layer.speed = 0.5
}, completion: { (finished) -> Void in
self.collectionView.forFirstBaselineLayout.layer.speed = 1.0
})
You can change UICollectionView layout.speed property, that should change animation duration of your layout...

Resources