How do I prevent a CALayer from redrawing as its bound change? - cocoa

i have a CALayer with a custom draw method I've added to my view's base layer. I set needsDisplayOnBoundsChange to NO. However, when I resize the parent view's frame, the layer's drawInContext: is getting called continuously. I'd like the contents to scale while the resize is occurring. Any clues?

Interesting, I have a case where I have a CALayer that correctly scales its contents until I call setNeedsDisplay on it to redraw its contents. One thing that may be different is that in my case the layer is being drawn by its delegate and not by a subclass of CALayer. Another thing that may be different is that this is on iOS and not OSX (I don't know which you are using in this case). It is possible that there could be behavior differences between subclasses and delegate drawn layers and/or iOS and OSX.
Another thing to note is that needsDisplayOnBoundsChange is documented to be NO by default, so one should not need to set it. I am not specifically setting needsDisplayOnBoundsChange on my layer.
You could try using a delegate to do the drawing to see if that makes a difference. Note that a UIView cannot be a delegate to a CALayer. In my case I made a simple delegate object that forwards the draw call to my view.

Related

Framework uses drawRect:; I need to use it on a layer-backed NSView. How to update?

According to the documentation for NSView's drawRect:
If your app manages content using its layer object instead, use the updateLayer method to update your layer instead of overriding this method.
I have an NSView with subviews that are provided by the framework, and they all draw using drawRect:. This framework-provided view is a subview of an NSView for which I require a layer. Because my framework-provided view is a descendant of a layer-backed view, drawRect: isn't usually called, especially in cases where the window is made active or inactive (the view needs to update to reflect its (in)active state).
Of course if I make my containing view not layer backed, updates occur when the window is made active or inactive.
Without modifying the framework into a custom fork, what's the best avenue for making sure drawRect: occurs when needed in my framework-provided view?
Thanks.
Edit 25-Aug-2018:
It looks like the trick is to set one of the views in the hierarchy to, e.g., [view setCanDrawSubviewsIntoLayer:YES, which according to the documentation uses all of the subviews’ drawRect: to add their drawing to its own layer. However this seems to work only through 10.13, and is broken in the 10.14 beta. I'll continue to look for a potential API change, unless this is a 10.14 beta bug.
Since the issue is still unresolved, it's not really answered yet.
Layer-backed views which don't override -wantsUpdateLayer to return true still draw themselves using -drawRect:. The bit of documentation you quoted is using "should" to mean "should, for best performance,". It's not required, it's just recommended.
Views don't generally redraw themselves just because the containing window has changed key or main status. You would have to mark them as needing display. Or the framework should be doing that.
I suspect the reason that it works when your view is not layer-backed is that you are marking your view as needing update. Since non-layer-backed views draw into the window's backing store using the painter model (back to front), if your view redraws itself then any subviews will have to redraw themselves on top of your view's drawing.
If the framework's views need to redraw when the window's key/main status changes, then they should be observing the relevant notifications and setting themselves as needing display. If they're not doing that, it's a framework bug. You can work around it by marking them as needing display yourself.

hosted CALayer in NSView bounds change

I have a custom CALayer hosted in an NSView, contained in an NSScrollView, setting properties on the CALayer will cause it's bounds to change.
The problem is that the NSView really wants to own the bounds of the contained CALayer, I can make it work by adding a KVO on the bounds so when the layer bounds changes it changes the NSView bounds (which changes the layer bounds to the same value - luckily KVO doesn't recurse if the value doesn't actually change).
Is there a better way? It would seem like going through the layout system would be better but then I can only specify my preferred frame (my bounds through my transform) - and this turns into setting my bounds, but with some amount of roundoff error.
How should a CALayer request a bounds change? Or is my current hack the preferred mechanism...
I’m not totally sure what you’re doing, but I’d have the CALayer set an NSSize ivar on its NSView which changes the NSView’s intrinsic size for autolayout, and then call setNeedsRelayout:YES on the view. Don’t try to have the tail wag the dog if you’re going to use a layer attached to a view.

Using CALayer as a background for other NSViews

I have an application with an NSTableView in a window. I want to use a CALayer as the background for the entire window, and the table view. In all my my experiments so far, the CALayer always draws over the NSTableView, which is not the effect I'm looking for. Is there a way to make this work, or am I simply out of luck due to the nature of layer-hosting views vs NSViews?
My test setup is a window with the usual NSScrollView/NSTableView combo, and a sibling NSView behind it in the view order. The NSView is set to be layer-hosting with my custom layer within it (just a layer with a backgroundColor set). I've experimented with setting the window's content view to be layer-backed, as well as the table view itself, as well as wrapping the NSScrollView in a layer-backed NSView. The result is always the same.
Thanks for any insight you might be able to provide.
It is simple. all overlapping views or layers should be layer backing or layer hosting for correct ordering.
you can set [tableview setWantsLayer:YES]
or simply check it in the layers tab when editing the interface.

Animating setHidden: on NSView via Cocoa bindings

I'm currently putting the final touches on a project.
A lot (if not all) of the UI logic currently relies on Cocoa Bindings.
Some of the user interface elements (labels, buttons, etc.) have their "Hidden" bindings defined. When certain events are triggered, these elements visibility is toggled.
I'm trying to animate the visibility change (by animating the opacity and maybe even the scale). This could easily be accomplished in a number of ways, either by setting the relevant layer properties, adding the animations to the layer, etc. However, since I'm trying to totally rely on the bindings behavior I "can't" really do this directly.
I tried an implementation using Layer actions, by defining actions for the keys kCAOnOrderIn and kCAOnOrderOut on the relevant elements, but it really didn't work, as the setHidden: is most likely being triggered on the NSView instead of the CALayer -- which makes sense.
So, my question is: how would you animate setHidden: on a NSView, when setHidden: is being invoked by the Cocoa Bindings.
Thank you.
This will fade out an NSView...
[[someView animator] setAlphaValue:0.0f];
Animating setHidden will have no visual effect since it's either on or off. If you want to animate visibility, use setAlpha (or setOpacity on the layer) instead. These take a value between 0.0 and 1.0. If you need the hidden flag to get set for the sake of state information, call -performSelector:withObject:afterDelay passing it a selector that sets the hidden value to whatever you need it to be after the animation has completed. Alternatively you can set up a delegate for explicit animation to be called back when the animation finishes and call setHidden then.
I would suggest taking a look at NSViewAnimation. It takes any NSView and can animate the frame, size or visibility.

Core animation code structure/conventions

In learning Core Animation, I learned very quickly that if you don't do it right, you get really weird undefined behavior. To that end, I have a few questions that will help me conceptually understand it better.
My NSView subclass declares the following in it's init. This view is a subview of normal layer backed view.
[self setLayer:[CALayer layer]];
[self setWantsLayer:NO];
After this, when and in what situations should I refer to self as opposed to [self layer]? I have been ONLY manipulating the layer with explicit and implicit animations, staying away from [self setFrame:] etc. and using [[self layer] setPosition] etc.
The problem with this approach is that the actual frame of the view stays in one spot throughout any and all animations applied. What if my view is supposed to recieve mouse events? For example, I have a view that uses core animation and it is dragged around by the mouse. Is there a way I can somehow keep the view frame synced with the current state of the presentation layer so I can receive proper mouse events?
About the presentation layer, apparently it's only available when an actual animation is in progress. Is there any sort of property of the layer that can tell me where it's ACTUALLY visually at even when an animation's not in progress?
I think you need to re-phrase your question a little. It seems there is some underlying misunderstanding, but you're not really expressing it very clearly. You're question title suggests you're looking to understand something more theoretical, but your actual question suggests you're looking for something more concrete. Let me see if I can clarify a few things.
The presentationLayer provides information about the layer's current state while "in-flight".
When there is no animation occurring, the presentationLayer and the layer information will be identical. Query the layer's bounds, frame, or position to find out where it is currently in its parents coordinate space.
NSViews must have layer backing enabled to be able to perform animations.
Make sure you're not just animating with an explicit animation and not actually setting the layer value that you're animating. Animations don't automatically change the properties of the layers they're animating. You have to change the property to the ending value yourself or it will just "snap back" to the starting value.
If you want to animate the view, as opposed to a layer, you can use the animator proxy, like [[view animator] setFrame:newFrame];
Wrap calls to the animator in a CATrasaction to alter things like animation duration.
Let me know if you need some clarification by updating your question. Providing some pertinent code would really help identify the problems you're having trouble solving.
Firstly, you want to use [self setWantsLayer: YES]. Also, it's only important to call -setLayer: before -setWantsLayer: if you want to provide a specific CALayer subclass (such as a CAScrollLayer); if you just want a regular CALayer you just call -setWantsLayer: and it'll be created for you. Even better, just check the 'wants layer' option in Interface Builder.
Secondly, the entire point of using a layer-backed view is that you can continue to use the regular NSView methods and get the free CoreAnimation 'tweening' effects. If you want to use CoreAnimation as your only means of moving items around, then the correct way to do so is to create a layer backed view which contains your pure-CALayer presentation hierarchy.
I've not looked at any freely-available CoreAnimation tutorials, but I can definitely recommend the Pragmatic Programmers' book on the subject. They also have a screencast available by the book's author.

Resources