Delay while moving CALayer with pan gesture - ios

I use pan gesture to move an image in CALayer. The issue I experience is that the image appears to move with a little delay and does not appear 'stuck' to my finger.
Here is the actual snippet of how I move the layer(facePic is the CALayer):
CGPoint translation =[touche locationInView:self.view];
self.facePic.frame =
CGRectMake(translation.x - self.facePic.frame.size.width/2,
translation.y - self.facePic.frame.size.height/2,
self.facePic.frame.size.width,
self.facePic.frame.size.height);

I think you see the result of implicit animation of a layer. If so then there are two options to disable this animation:
use transactions
set layer actions
To use transactions wrap your code with CATransaction
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
. . .
[CATransaction commit];
To disable some layer actions you can add this to the layer init, the position animation for example:
aLayer.actions = #{#"position":[NSNull null]}; // FIXED property name

Related

CALayer with zero speed doesn't change it's position

I'm implementing a custom pull-to-refresh component.
Create CALayer with a spinner animation, than add this layer to UICollectionView. Layer's speed is zero (self.loaderLayer.speed = 0.0f;) and layer animation is managed with timeOffset in scrollViewDidScroll:. Problem goes here, because I also want to show a loader always in the center of pulling space, so I change layer's position in scrollViewDidScroll: like this:
[self.loaderLayer setPosition:CGPointMake(CGRectGetMidX(scrollView.bounds), scrollView.contentOffset.y / 2)];
But nothing happens with layer position (calling [self.loaderLayer setNeedsDisplay] doesn't help). I understand that it's because zero speed. And currently I found the way which works (but I don't like that):
self.loaderLayer.speed = 1.0f;
[self.loaderLayer setPosition:CGPointMake(CGRectGetMidX(scrollView.bounds), scrollView.contentOffset.y / 2)];
self.loaderLayer.speed = 0.0f;
How could I change a position for a paused layer right? What am I missing?
All regards to #David for the reference. I just summarize it as an answer.
CoreAnimation works with two kinds of animations (transactions): explicit and implicit. When you see Animatable word in property documentation, it means that each time you set this property, CoreAnimation will animate this property changes implicitly with system default duration (default is 1/4 second). Under hood CALayer has actions for these properties and calling -actionForKey returns such action (implicit animation).
So in my case, when I change a layer position, CoreAnimation implicitly try animating this changes. Because layer is paused (speed is zero) and animation has default duration, we don't see this changes visually.
And answer is to disable implicit animations (disable calling layer -actionForKey). To do that we call [CATransaction setDisableActions:YES].
OR
We can mark this animation as immediate (by setting it's duration to zero) with calling [CATransaction setAnimationDuration:0.0];.
These flags/changes are per thread based and work for all transactions in specific thread until next run loop. So if we want to apply them for a concrete transaction, we wrap code with [CATransaction begin]; ... [CATransaction commit]; section.
In my case final code looks
[CATransaction begin];
[CATransaction setDisableActions:YES];
[self.loaderLayer setPosition:CGPointMake(CGRectGetMidX(scrollView.bounds), scrollView.contentOffset.y / 2)];
self.loaderLayer.transform = CATransform3DMakeScale(scaleFactor, scaleFactor, 1);
[CATransaction commit];
And it works perfectly!

Getting a smooth rotation using pan gesture recognizer

I am rotating a CATransform3D layer around y-axis by taking input from UIPanGestureRecognizer:
CGPoint movement = [recognizer translationInView:self];
CGFloat trackSpread = self.bounds.size.width;
CGFloat angle = (35 * abs(movement.x))/trackSpread;
transform = CATransform3DRotate(transform, degToRad(angle), 0, 1, 0);
The view rotates smoothly if the pan is done slowly i.e. receives continuous x values from recognizer but if the panning is at a faster pace i.e. the values receives from recognizer are not continuous I see jumps in the rotation (kind of like the layer is also translating a bit around its rotation point). Would what be the way to always keep the rotation smooth whether panning is done slow or fast?
Stand alone layers have implicit animations that happen automatically just by changing the property. This is what you are seeing. When continuously updating the property it looks like the layer is lagging and falling behind.
If you want to change the property without having the implicit animation you can change it within a transaction that has disabled all actions (a more general name for animations):
[CATransaction begin];
[CATransaction setDisableActions:YES]; // no animations
myLayer.transform = newTransform;
[CATransaction commit];
You can also configure the layer to never look for an animation for a given key path by adding NSNull to its actions dictionary for that key path:
myLayer.actions = #{
#"transform": [NSNull null] // never animate "transform"
};

The movement of view with applied transformation on iPad Air (with retina display)

If I apply transformation for a view, then movement of the view on iPad Air happens with lags. It looks like implicit animations in CALayer.
I've created test project. It should be executed on iPad simulator.
This is code that I use for apply transformations:
CGAffineTransform transform = CGAffineTransformIdentity;
if (_transformSwitch.on)
{
transform = CGAffineTransformRotate(CGAffineTransformMakeScale(2.0, 2.0), M_PI / 3.0);
}
self.frameView.transform = transform;
This is code that I use for apply movement:
CGPoint transition = [_panRecognizer translationInView:self.view];
CGPoint newCenter = CGPointMake(_startCenter.x + transition.x, _startCenter.y + transition.y);
self.frameView.center = newCenter;
How I can move the view with applied transformations and avoid the animations?
UPD
I've found a solution for a moving by timer invocation, but if I'm moving frame by a finger, I've have problem with the animation.
I set a center of the view with wrapping it with [CATransaction begin] [CATransaction commit]:
[CATransaction begin];
[CATransaction setValue:#(YES) forKey:kCATransactionDisableActions];
_destinationIndicatorView.center = center;
self.frameView.center = center;
[CATransaction commit];
I've found strange solution, but I want to know why is it a worked solution?
I added to this method setNeedsDisplay, and it's solved my problem. (If I add any view over my screen with drawRect method, and call setNeedsDisplay on it view, it's also solved my problem):
[CATransaction begin];
[CATransaction setValue:#(YES) forKey:kCATransactionDisableActions];
_destinationIndicatorView.center = center;
[self.frameView setNeedsDisplay];
self.frameView.center = center;
[CATransaction commit];
I've updated my project.
UPD2
This lag happens only on iPad with retina display (for example iPad Air). I've created small video to illustrate the problem: https://yadi.sk/i/rAneCEFmjjEej
I think you could wrap the changes in this code:
[CATransaction begin];
[CATransaction disableActions];
// Your changes to the UI elements...
[CATransaction commit];
Alright, I've played around with your project and found 2 things that do not what you expected they would do:
The 1st one is drawRect: on FrameView:
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
}
From Apple:
The default implementation of this method does nothing. Subclasses
that use technologies such as Core Graphics and UIKit to draw their
view’s content should override this method and implement their drawing
code there. You do not need to override this method if your view sets
its content in other ways. For example, you do not need to override
this method if your view just displays a background color or if your
view sets its content directly using the underlying layer object.
The 2nd one is the wrong (it's right according to the doc, see below) return value of the overridden - (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event on FrameView: you return nil (as described in the CALayerDelegate reference) while CoreAnimation, in fact, expects an instance of NSNull (as might be assumed when reading CALayer class reference). Your actions led to an unintended result as there's a collision in the documentation. I'm talking about this and this.
P.S. As a result, I made it working without using CATransaction. Unfortunately, I don't have a retina iPad by hand, so please check out my changes to your GitHub project (I will create a pull request containing all my changes) and tell if it resolves both the unintended animation (assuming resolved) problem and the lag of dragging (need to check for performance issues on a real iPad).

How to disable the animation when changing the transform property of UIView?

As apple document said: 'transform
Specifies the transform applied to the receiver, relative to the center of its bounds.
#property(nonatomic) CGAffineTransform transform
Discussion The origin
of the transform is the value of the center property, or the layer’s
anchorPoint property if it was changed. (Use the layer property to get
the underlying Core Animation layer object.) The default value is
CGAffineTransformIdentity.
Changes to this property can be animated. Use the
beginAnimations:context: class method to begin and the
commitAnimations class method to end an animation block. The default
is whatever the center value is (or anchor point if changed)'
I don't need the animation ,how to disable the animation when changing the transform property of UIView?
You can disable the implicit animations this way:
[CATransaction begin];
[CATransaction setDisableActions:YES];
// or if you prefer: [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
// Your code here for which to disable the implicit animations.
[CATransaction commit];
https://developer.apple.com/library/ios/documentation/GraphicsImaging/Reference/CATransaction_class/Introduction/Introduction.html
It (should) only animate when you change the transform property inside e.g. UIView animateWithDuration: block.
I.e. disabling animation can be achieved by simply not changing the transform property inside an animation part of your code.
Can you post some code where you get animations that you didn't expect?

Getting subview location after superview rotation

I have a UIView which has several subviews that respond to touch. If I rotate the layer of the superview with some code like this:
[CATransaction begin];
[CATransaction setDisableActions:true];
view.layer.transform = CATransform3DRotate(view.layer.transform, angle, 0, 0, 1);
[CATransaction commit];
... then I can't seem to find the right location of the subviews when responding to touch events... Any ideas?
I found it, worked out beautifully I guess. I needed to convert points between UIViews for doing bezier hit testing:
-(BOOL)containsPoint:(CGPoint)point
{
point = [self.superview.superview convertPoint:point toView:self];
return [self.slicePath containsPoint:point];
}

Resources