quickly showing and hiding CALayer's appears to be slow - ios

In my prototype app there are around 100 CALayers at different but fixed positions with the same small image as the content. The only thing necessary now is to toggle the hidden property repeatedly and very quickly.
This works, but it's noticably slower than with my previous approach using UIImage's drawAtPoint: method in drawRect.
I want a strobe-like look, no transitions. That's why I'm using hidden and not opacity, but still, it kind of looks like there's a fade and that tells me it's slow.
With the drawAtPoint:-approach it looked good, but it was heavy on the CPU.
Fo this reason I rewrote it using CALayer and now I'm puzzled why it is that slow.
Can you give me advice how to investigate this?
With Instruments I didn't gain any insight. It tells me it's rendering at 59-60 FPS but visibly it's much slower.
It seems like there's a delay between the (touch) events and the hiding or showing of the layers taking effect.
That's how I'm initializing the layers:
layers[i] = [CALayer layer];
layers[i].frame = frameForLayer(i);
layers[i].contents = (__bridge id)image;
[layers[i] setContentsScale:scale];
layers[i].hidden = YES;
[[self layer] addSublayer:layers[i]];
All that in the awakeFromNib in my main view.
Later, only the hidden properties are modified, the rest stays.
EDIT:
Instead of just the someLayer.hidden = NO, I'm now writing
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue
forKey:kCATransactionDisableActions];
someLayer.hidden = NO;
[CATransaction commit];

try doing the above code in a CATransaction block and set the animation duration like so:
[CATransaction setValue:[NSNumber numberWithInt:0] forKey:kCATransactionAnimationDuration];
you may also need to do disable transitions like so:
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
I believe CALayers have a default 'animations' for when you set their contents.

Related

CALayer behaves differently when creating anew vs when applying CATransform3dIdentity

I am really stuck with some CALayer knowledge missing.
I have a layer which knows how to draw an arrow.
I apply some transform on every frame on it.
But it only works as expected if I always create this layer from scratch and apply the transform. And it doesn't work if I try reusing the same old layer (by applying a CATransform3dIdentity). Through "it doesn't work" I mean it flickers on the screen and the transform is not applied as needed as compared to the transform applied to the newly created layer.
My code looks as follows:
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection
{
transformModel = [OverlayTransformExtractor transformFromPixelBuffer:sampleBuffer];
if(transformModel)
{
//I tried also not removing it every time but just transforming.. the effect is the same
[firstLayer removeFromSuperlayer];
if(!firstLayer)
{
firstLayer = [VNArrowLayer layer];
firstLayer.frame = CGRectMake(videoView.frame.origin.x, videoView.frame.origin.y, videoView.frame.size.width, videoView.frame.size.height);
originalTransform = firstLayer.transform;
}
//resetting the previous transforms
firstLayer.transform = CATransform3DIdentity;
[self applyTransform:transformModel.transform];
dispatch_async(dispatch_get_main_queue(), ^(void)
{
[arrowView.layer addSublayer:firstLayer];
[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue
forKey:kCATransactionDisableActions];
[firstLayer setNeedsDisplay];
[CATransaction commit];
});
}
else
{
dispatch_async(dispatch_get_main_queue(), ^
{
[firstLayer removeFromSuperlayer];
[secondLayer removeFromSuperlayer];
});
}
}
Some suggestions of why is it behaving differently when it's just created? I checked the transform to be the same. I even tried to set the same position as a new layer (0,0), but the it is even more weird (it remains in the left top corner)
I also thought that maybe it's because of the implicit animation, so I tried turning it off -> no change.
Adding a new layer every time is not acceptable. The memory becomes full pretty fast, even though I manually try setting the firstLayer to nil.
So as stated above when I was applying a transform to existing layer (at every frame) it flickered. When creating it at every frame it was perfect, but memory intensive.
I could not get what makes it flicker, maybe some constraints or layout stuff about which I cannot find any information but..
I managed to make the approach (new layer at every frame) work. The idea was, I was removing the old layer and assigning nil to it not on the main thread, which cause the deallocations to happen late after and in this way to fill my memory quickly.
So the solution is to remove the old layer, to create the new one and to apply the transform on the main thread.
If smb can provide an answer that explains the flickering, I'll be glad to mark it as a correct one.

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!

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 can I change sublayers of animation layer when using AVVideoCompositionCoreAnimationTool

I want to change the parent layer's sublayers dynamically when using AVVideoCompositionCoreAnimationTool. I noticed that sublayers is an animatable property accordingto 《Core Animation Programming Guide》, but still can't figure it out how to achieve that. Any idea? Thanks
I don't know about AVVideoCompositionCoreAnimationTool, but in general it works like following Code. It will show an animation when removing and adding at the new parent layer. The action identifiers if you'd like to change them are kCAOnOrderIn and kCAOnOrderOut.
CALayer *layerToMove = ....;
CALayer *newParent = ...;
[CATransaction begin];
[layerToMove removeFromSuperLayer];
[newParent addSublayer:layerToMove];
[CATransaction commit];

CAlayer transform on sublayers flickers with gestures (ipad)

I have a CALayer that contains few other subLayers (CATextLayer actually)
I want to apply some transformation on that layer when user do usual gesture on the ipad but it doesn't seem to be working properly. The goal of using the CALayer was to apply the transformation only to that Layer so that all of my sub-textLayer would be affected at the same time with the same transformation.
What's happening is that the transformation seem to flicker between previous and current position. I don't really get what could be the problem... When I do a 2 fingers panning gesture for example, CaTextLayer positions flickers all the time during my gesture and at the end they are all correctly placed at their new translated position.
So everything seems to work fine except that flickering thing that is bothering me a lot.
Do I need to set some property I don't know about ? I'm thinking it might have something to do with bounds and frame too....
Here's how I create my CATextLayer (This is done only once at creation and it works properly):
_textString = [[NSString alloc] initWithString: text];
_position = position;
attributedTextLayer_ = [[CATextLayer alloc] init];
attributedTextLayer_.bounds = frameSize;
//.. Set the font
attributedTextLayer_.string = attrString;
attributedTextLayer_.wrapped = YES;
CFRange fitRange;
CGRect textDisplayRect = CGRectInset(attributedTextLayer_.bounds, 10.f, 10.f);
CGSize recommendedSize = [self suggestSizeAndFitRange:&fitRange
forAttributedString:attrString
usingSize:textDisplayRect.size];
[attributedTextLayer_ setValue:[NSValue valueWithCGSize:recommendedSize] forKeyPath:#"bounds.size"];
attributedTextLayer_.position = _position;
This is how I add them to my Super CALayer
[_layerMgr addSublayer:t.attributedTextLayer_];
[[_drawDelegate UI_GetViewController].view.layer addSublayer:_layerMgr];
And here's how I apply my transformation :
_layerMgr.transform = CATransform3DMakeAffineTransform(_transform);
After lots of reading and testing...I've found my own solution.
It looks like the CoreAnimation uses default animation when you do transformation or operation on any layers. It's very recommended that when you do such CALayer operation that you go through what they call "Transactions".
I've found all about that in the CoreAnimation Programming guide, under the section : transactions.
My solution was then to implement such a transaction, and preventing any animation while doing CALayer operation.
This is what I do when applying my transformation (which prevents flickering) :
-(void)applyTransform
{
if (! CGAffineTransformIsIdentity(_transform))
{
[CATransaction begin];
//This is what prevents all animation during the transaction
[CATransaction setValue:(id)kCFBooleanTrue
forKey:kCATransactionDisableActions];
_layerMgr.transform = CATransform3DMakeAffineTransform(_transform);
[CATransaction commit];
}
}

Resources