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.
Related
If I create a blank Mac XCode project and layout 500 simple NSView objects side by side in the main window it loads pretty damn fast. If I set wantsLayer=YES on each subview, performance dramatically drops, by several seconds. Why is this the case conceptually? It seems that layers would be faster not slower than regular old NSViews.
You're giving the system more work to do by layer-backing so many views. Layer-backing allows graphic acceleration (for drawing) but it adds a bit of overhead to things like layout, not to mention just creating them and putting them on screen. If used properly, it's not really much of a problem.
Typically, if you had so many "things" to manage on screen at once, you'd have one layer-backed hosting view that manages its own tree of sublayers. "But what about view-based table views?" you ask. Trickery, trickery, I say! Table views don't actually keep all the cell views they manage around; they efficiently reuse them, keeping around only enough to represent what's on screen and/or animating around.
So I'd say this isn't really a problem since it's not a particularly good approach to throw 500+ layer-backed views up for layout and drawing to begin with. :-)
as of 2021, Joshua Nozzi's answer is just the half of the truth.
When you want to utilise that much layers, you should make use of the power of CALayers with Sublayers instead of using NSViews over and over again, each with its own NSCell and possible CALayer backed as you forced it to with -wantsLayer:.
There is nothing wrong with 500 sublayers. Sublayers allow you to be fast when properly structured.
If you want to speed up even more, turn off autoanimation by force on each Layer that does not need it. Yes a lot examples out there show that you can turn off Autoanimation each time you call some property to be changed, slowing down your drawing even more, because it involves even more object messaging.
Keep in mind the following example turns off Auto-animated Keys by their Key name and kicks out the whole action and does not apply onto all Sublayers on purpose. If your structure of Sublayers of Sublayers need it you could call this on according ParentLayer to wipe out the CPU eating Autoanimation. Its much better to let the autoanimation do its job on the layers you actually want it to. Which turns around Apples default paradigm to animate everything. If most action keys are turned off be aware that your layers do show up kind of like a state machine. In the example here #"frame" animations are not wiped out..
void turnOffAutoAnimationsForLayerAndSublayers(CALayer* layer) {
NSNull *no = [NSNull null]; //(id)kCFNull
NSDictionary *dict = [NSDictionary dictionaryWithObjects:#[no,no,no,no,no,no,no,no]
forKeys:#[#"bounds",#"position",#"contents",#"hidden",#"backgroundColor",#"foregroundColor",#"borderWidth",#"borderColor"]];
for (CALayer *layertochange in layer.sublayers ) {
if (layertochange.sublayers.count) {
for (CALayer *sublayertochange in layertochange.sublayers) {
sublayertochange.actions = dict;
}
}
layertochange.actions = dict;
}
}
and use it in your CALayer backed NSView -init like..
CALayer *layer = [CALayer layer];
layer.frame = self.frame;
//...backgroundColor, contents, etc etc..
// here you could even do a for loop adding as much sublayers you want..
for (int y=0; y<100; y++) {
CALayer *sub = [CALayer layer];
//...frame, backgroundColor, contents, of sublayers etc etc..
layer.frame = CGRectMake(0, 0, 10, y*10);
[layer addSublayer:sub];
}
// structure is ready, add it to the base layer
self.layer = layer;
// now tell NSView you really want to make use this layer structure
self.wantsLayer = YES;
// turn off AutoAnimations
turnOffAutoAnimationsForLayerAndSublayers(self.layer);
For layers that are basically all the same there is even a specialised Subclass called CAReplicatorLayer. Allowing you to add even more with less internal draw calls.
You should also know that layers that are hidden are not calculated at all (meaning drawing here). You can still change properties even on hidden Layers. Unhide them, when needed, not earlier. So in example a custom CATextLayer that will change its string property to lets say #"", aka nothing, will still be drawn. But if you Subclass CATextLayer and change its string implementation to lets say..
#interface YourTextAutoHideLayer : CATextLayer
-(void)setString:(id _Nullable )string;
#end
#implementation YourTextAutoHideLayer
-(void)setString:(id _Nullable)string {
self.hidden = [string isEqual:#""];
super.string = string;
}
#end
you speed up drawing even more. A) because less object messaging to hide the layer that has no text anyway and B) once hidden it is not part of the internal draw calls effective speeding up your main CALayers drawing.
On NSTableViews that are NSCell based you usually never use CALayers. There is no need to, NSTableViews manage the visible Cells on purpose to keep in example scrolling smoothly. And still they (NSTableViewCells) have a reuse-mechanism to speed things up. You will very likely not re-invent a tableview with CALayers anyway. But in case you do reinvent, there is a CAScrollLayer class too.
And if that is not enough speed it is worth thinking about a MetalView utilising the power of GPUs.
Edit: code your CALayers not in -drawRect:. -drawRect: is called anytime something changes, in example frame/position on screen or bounds etc.. so try to avoid coding CALayers in there. You can set [self setNeedsLayout:YES]; & [self setNeedsDisplay:YES]; on purpose for a good reason, the reason is you want to avoid too much drawing for basically no change at all. -drawRect: in example was/is such method that is supposed to handle a backing with its own context to draw in. As CALayers have their very own mechanism you can let the drawRect block blank, doing nothing in there, maybe even erasing the method at all if you dont need it.
CALayers are not part of the auto layout system unless you code in -layout.. so again, keep CALayer drawing outside such and you are good. and pssst a lot CALayers properties can be changed even outside the main thread, some definitely not like layer.string.
I have many layer-backed views contained in a NSScrollview and am predominantly concerned with scrolling performance. The documentView and higher are layer-backed hence their subviews are also layer-backed.
The following are the three natural places that display/rendering code could go:
override NSView.wantsUpdateLayer to return false (or don't do anything because this is the default) & do drawing in NSView's drawRect method
override NSView.wantsUpdateLayer to return true & do drawing in NSView's updateLayer method
do NO drawing in NSView at all & perform all drawing in CALayer's drawInContext method
From the WWDC 2013 Session 215 talk it was stated that (2) is more performant than (1) because the view then doesn't need to create a temporary store for the drawRect output. Firstly, I don't have 100% clarity on when "backing stores" are made and when not and Secondly how do (2) and (3) compare and when might you use one over the other?
In particular, I have to draw text into my view. How would I go about doing that in the updateLayer call? The only examples of drawing text seem to need to get hold of a context - which isn't naturally available in updateLayer.
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.
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.
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.