CALayer flickers when drawing a path - cocoa-touch

I am using a CALayer to display a path via drawLayer:inContext delegate method, which resides in the view controller of the view that the layer belongs to. Each time the user moves their finger on the screen the path is updated and the layer is redrawn. However, the drawing doesn't keep up with the touches: there is always a slight lag in displaying the last two points of the path. It also flickers, but only while displaying the last two-three points again. If I just do the drawing in the view's drawRect, it works fine and the drawing is definitely fast enough.
Does anyone know why it behaves like this? I suspect it is something to do with the layer buffering, but I couldn't find any documentation about it.

[UIView new] is simply shorthand for [[UIView alloc] init].

Give the following before setNeedsDisplay method::
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:someDelay] forKey:kCATransactionAnimationDuration];
[aLayer setNeedsDisplay];
[CATransaction commit];

You might have better luck using a layer hosting view.
Instead of using the drawLayer:inContext: method, setup the view you want and add a CALayer to it:
UIView *layerHosting = [[UIView alloc] initWithFrame:frame];
[layerHosting setLayer:[[CALayer new] autorelease]];
Hope that helps!

Related

Why does Cocoa view order overlaying behave strangely with AVCaptureVideoPreviewLayer in OSX?

I have a view controller controlling a main view with to sibling subviews. One is a view I call overlayView that has some buttons and labels and things from IB. The other is one called captureLayer that I programmatically add to hold an AVCaptureVideoPreviewLayer from a webcam. For some reason I can't get the overlay layer to show on top of the preview layer.
This doesn't work:
capturePreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:captureSession];
[self setCaptureView:[[NSView alloc] initWithFrame:[[self view] bounds]]];
// I know the order here is important and I think this is right
[[self captureView] setLayer:capturePreviewLayer];
[[self captureView] setWantsLayer:YES];
[[self view] addSubview:[self captureView] positioned:NSWindowBelow relativeTo:[self overlayView]];
[captureSession startRunning];
But I found out that putting the capturePreviewLayer in the main view, so the overlayView is a child instead of sibling to its view, does work:
capturePreviewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:captureSession];
[[self view] setLayer:capturePreviewLayer];
[[self view] setWantsLayer:YES];
[captureSession startRunning];
Any idea why? I've even done a very analogous thing using UIViews in iOS and didn't see anything weird like this.
This is probably because your captureView is layer backed or layer hosting, while your overlay view is not. The overlayView draws directly to the window layer, and the captureView draws into a new layer on top of that.
The solution is to make the overlayView layer backed as well and use it's zPosition if needed.
On iOS every view is layer backed by default so this problem won't manifest itself there.

Animating label (NSTextField) horizontally

I have a NSTextField as a label, showing a string.
I want to animate this label from right to left, if the content of it is too large to be displayed at once.
I've done this with an NSTimer so far, it works, but it's just not a very good solution.
The labels are displayed in an NSTextFieldCell, in a Table View.
They often get out of sync, and I guess it's just eating up a lot of CPU/GPU resources.
Is there another way with Core Animation to do this?
I have tried it with layers, as you can see right here:
CALayer and drawRect
but I didn't get it working either.
I would really appreciate your help.
You can simply animate the position of NSTextField with animator like
[[textField animator] setFrameOrigin:NSMakePoint(x,y)];
you can also embed it in "CATrancation" code like this:
[CATransaction begin];
[CATransaction setAnimationDuration:0.5];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
[[textField animator] setFrameOrigin:NSMakePoint(x,y)];
[CATransaction commit];
if you need animation delegate, you can use CABasicAnimation
CABasicAnimation* animation = [CABasicAnimation animation];
animation.delegate = self;
NSDictionary *animations = [NSDictionary dictionaryWithObjectsAndKeys:animation,#"frameOrigin",nil];
[textField setAnimations:animations];
[[textField animator] setFrameOrigin:NSMakePoint(x,y)];
Delegate methods are
- (void)animationDidStart:(CAAnimation *)theAnimation;
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag;
If you need to mask your text field, just embed it in other NSView.
First, animate the label using one of the functions offered in the other answers.
Then, if you want to display another view on the sides without overlapping, you can:
Insert the label in a subview with the limits you wish
Use bringSubviewtoFront: or sendSubviewToBack: to make sure your label stays in the back

How to add a CALayer to an NSView on Mac OS X

I'm trying to learn how to use and implement CALayer in a Mac Objective-C application, but I can't seem to probably do the most basic thing - add a new layer and set its background colour/frame size. Can anyone see what is wrong with my code?
CALayer *layer = [CALayer layer];
[layer setFrame:CGRectMake(0, 0, 100, 100)];
[layer setBackgroundColor:CGColorCreateGenericRGB(1.0, 0.0, 0.0, 1.0)];
[self.layer addSublayer:layer];
[layer display];
I put this in the - (void)drawRect:(NSRect)rect method of my custom NSView subclass, but when I run the application, it just shows a blank view, with no background colour or evidence of the layer I created.
First of all, you don't want to add a layer in the drawRect: method of a view, this gets called automatically by the system and you'd probably end up with a lot more layers than you actually want. initWithFrame: or initWithCoder: (for views that are in a nib file) are better places to initialize your layer hierarchy.
Furthermore, NSViews don't have a root layer by default (this is quite different from UIView on iOS). There are basically two kinds of NSViews that use a layer: layer-backed views and layer-hosting views. If you want to interact with the layer directly (add sublayers etc.), you need to create a layer-hosting view.
To do that, create a CALayer and call the view's setLayer: method. Afterwards, call setWantsLayer:. The order is important, if you'd call setWantsLayer: first, you'd actually create a layer-backed view.
You need to make a call to the "setWantsLayer" method.
Check out the following documentation for the description for setWantsLayer:
https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ApplicationKit/Classes/NSView_Class/Reference/NSView.html
In a nutshell, your view needs to be layer-hosting view. Because it is a layer-hosting view, you should interact with the layer, and NOT interact with the view itself and don't add subviews to it.
[self setLayer:[CALayer new]];
[self setWantsLayer:YES]; // the order of setLayer and setWantsLayer is crucial!
[self.layer setBackgroundColor:[backgroundColor CGColor]];
Put this out of the drawRect. I normally put my layer setup in either the init method or the viewDidLoad.
Otherwise anytime the view is drawn a new layer is added and allocated. Also I've never used the [layer display] line before. The docs actually tell you not to call this method directly.
Updated info (Swift): first call view.makeBackingLayer() then set wantsLayer to true.
https://developer.apple.com/documentation/appkit/nsview/1483695-wantslayer

Force a view to redraw

I tried to use setNeedsDisplay, but that is very much up to the mercy of the system whether to refresh right the way. Currently I remove and add the subview every time, so the latest content is forced to show. The code works, however it lacks gracefulness.
[myView removeFromSuperview];
[myView release];
myView = [[MyView alloc] initWithFrame:CGRectMake(0.0, MY_VIEW_Y, 320.0, MY_VIEW_H)];
[self.view addSubview:myView];
//[self.myView setNeedsDisplay];
[self.view bringSubviewToFront: myView];
Your view should be getting redrawn at the next drawing cycle. Is this not happening or is this too slow for you? ... i.e.: how fast do you need it to redraw and what latency are you seeing between calling setNeedsDisplay and drawRect being called?
If you need more precise control of the drawing, you may need to use a view backed by CAEAGLLayer, in which case it runs from openGL and setNeedsDisplay has no effect.

CATransaction duration not working

I set a few CALayer transform and bounds modifications, within a CATransaction. However, regardless the method I use (key-value, setAnimationDuration) there is no animation, the changes are done, but immediately without transition.
Do you have any idea why?
Thanks!
/* CALayer*layer=[CALayer layer];
layer.bounds =AnUIImageView.bounds;
layer.contents=AnUIImageView.layer.contents;
[AnotherUIImageView.layer addSublayer:layer];
CGPoint thecentre=AnUIImageView.center;
CALayer* layerInTarget=[AnotherUIImageView.layer.sublayers lastObject];
[layerInTarget setPosition:[self.view convertPoint:thecentre toView:AnotherUIImageView]];
AnUIImageView.layer.hidden=YES;
*/ // the code above works, i show it to be complete
[CATransaction begin];
[CATransaction setValue:[NSNumber numberWithFloat:2.0f]
forKey:kCATransactionAnimationDuration];
layerInTarget.position=[self.view convertPoint:AnotherUIImageView.center toView:AnotherUIImageView];
layerInTarget.transform=CATransform3DMakeScale(0.6,0.6,0.6);
[CATransaction commit];
CALayers that are associated with a UIView (as in, they're accessed via view.layer) do not participate in implicit animations, regardless of how you configure your CATransaction. You either need to use explicit animations (using the appropriate subclass of CAAnimation) or you need to use UIView animations.