NSView and wantsLayer; continually switching the value

I have discovered a behavior in NSView that is unwanted. It does not seem to be a "bug", I'm just doing something that was not envisioned by the designer of layerBacking. and I'm trying to determine the best way to work around it.


in the design, my NSView subclass needs to be able to host ui that is both based completely on CALayer, or NSViews. in english: I need to be able to load IB hierarchies, or CALayer hierarchies, into a single NSView, at will.


for this, I have a system that presumed that I could set the "wantsLayer" bool in the View class to remove layer backing.

true for when I'm using layers

false for when I just want to use a view hierarchy.


I have discovered that I can set it to true only 1 time. the very next time I set wantsLayer to true, there is no layer Backing applied to the the view. In other words, there is no layer instance found at NSViewInstance.layer?


In the past I have experienced that you can seriously mess up a View by setting it's layer, and I had an elaborate approach to handling the View's layer when switching between layer-backing and not layer-backing: I would store the original backing layer, and replace it with a new one, while I was working directly with Layers, and as I was turning Off layerBacking, I would just replace that layer with the original layer.


I've done some refactoring, and in place of that elaborate setup, I've just changed the way I handle layers. I no longer ****** the View's layer, except to add a single layer to it's sublayers, that I can then change as necessary. eventually removing that single sublayer, leaving the view's layer as I found it. This is not currently working.


So I'm here asking the question: what are the best practices for avoiding damage to the NSView classes handlig of layerBacking so that you can turn layerbacking on and off at will?

Accepted Reply

I think I solved my question.


I did a playground where I messed around with the concepts in a more abstract way. I discovered that : you really should replace the view's layer, under every circumstance, if you intend a mixed use like I do. Otherwise, you wind up with a jacked up layerBacked view.


the View maintains the view's layer all the time, after you set wantsLayer = true, the first time. So the only way to be certain you haven't broken anything, is to replace the backing layer. This is kind of the same thing as having a view hierarchy, and it has less over head. So, my question is answered.


in addition to that, I found that I shouldn't be getting an empty View.layer property. so I doubled down on my efforts to track down that bug.

it appears that the way I was handling the setup, and the un-setup of the View's layer was being called out of order. So, we very quickly got into a position where we are setting the view's layer before we've saved it. This was because I was relying on the properties' didSet subMethod to set the view back to default state, and I was instantiating the incoming controller As I was setting it... and the setup occurs at initialization. So my View's layer gets swapped out by the incoming controller's setup. the View notices it WILL be changing the controller so it tries to set the View to default, and it wipes out the new Controller's layer from the view. In the Next pass of changing the view's layer, it gets set to nil. I had to add print("") calls to every load/unload method in the app, before I saw that.

Replies

Why not create two similar views, one with layer, the other without, that have the same content, and switch from one to other, by hiding / showing ?


Of course, need to manage data to update each other in background.

@Claude31

it might go that way (or in a direction just as complicated)

but the deal is that it's not a binary proposition. I have multiple use cases that employ both view hierarchies, and layer hierarchies. and I will have multiple instances of these views. Part of what I'm doing in the refactor is trying to cut out overhead (code got crufty while I was worrying about other things.) 3 Views (because it would have to be a parent view with 2 subViews), plus multiple controllers replacing a single View and a single controller... that's a significant increase in overhead. I'd like to avoid that if at all possible (and of course, we know that it is possible, because I have done it that way.)


but I am refining my understanding of my issues. in the last handful of hours I've discovered that I am getting a ghost instance of the layer.

I test my view to see if it has a layer. and the test comes back as true. Then I set the frame of that layer's sublayer. this is done when the view is resized. can't have a layout manager applied to the view's backing layer. Well, after going through the process once (wantsLayer = false, wantsLayer = true, wantsLayer = false) and then resizing the View, the test for the View's layer returns false. But, if you look in the debugger, there's a layer in there.


so then I tried to implement the process of reserving the ViewBacked Layer:

if self.needsLayer{
            self.view.layer = baseLayer
            self.view.wantsLayer = false
        }


I store the view backed layer as a local property in my controller. and I replace that layer with a Newly instantiated CALayer, which is just the default


now as we test for the View's layer, we get a false... and I am now about to try it, with that new layer as a property in my viewController class. (because I think garbage collection was taking out a local scoped CALayer, that I set as the layer of the View... ok hold on a sec... ok... this is odd. now the app will not compile as a related observation has become "Type of expression is ambiguous without more context"


viewSizeObsrvr = view.observe(\.viewFrameUpdated, options: [.old,.new]){(obj, change) in
            /
                self.panelManLayerController!.layer.frame = self.workLayer
            /
        }


worked just fine (this is how I get notification that the view's size has changed) As soon as my base ViewController class , defined this property :


open var workLayer : CALayer = CALayer()


the subclass's observation stopped working. wierd. might take me some time to figure this out.

ugh. found that issue. fixed it.

and I have the same issue. need to run some tests to see if :

1. my newly created workLayer is the View's layer.

2. if the View is updating it's layer's size.

we are certainly losing the view's layer. ok... this is the code I'm using to set the view's layer:


if self.needsLayer{
            self.view.wantsLayer = true
            baseLayer = self.view.layer
            self.view.layer = self.workLayer
        }


and to undo that, when the view is reset to it's defaults after my subController object is removed:

if self.needsLayer{
            self.view.layer = baseLayer
            self.view.wantsLayer = false
        }

I've done some very specific testing, and the results indicate a scope problem.

in the method where I set the view's layer, I can access it like this: view.layer?

that will always return the CALayer object.

becuse I am setting the View's layer with a property of my class (which is imported from a framework) defined thusly:

open var workLayer : CALayer = CALayer()

(I should trying to make it @objc and dynamic... but that shouldn't have anything to do with this because...)


it works the first go around. the very first controller has no issues. Subsequent controllers (which are initialized just prior to usage. there are no controllers that we keep around, so each workLayer : CALayer is brand new) we get the issue... which means in the method where we set the View's layer, it is a property of the view (view.layer) but after that point it is no longer retained by the view. (in the debugger, view.layer = 0x0) But it is NOT garbage collected. it still exists, as a property of my controller layer. I've done a global check to see if any code that resets the view is called right after setting the layer, and came up empty.


this is really weird. I expect it to operator error, but I'm not changing the value, and (i don't think) that I've intorduced any irregularities that would keep the view from accepting the workLayer as it's new backing layer.

I think I solved my question.


I did a playground where I messed around with the concepts in a more abstract way. I discovered that : you really should replace the view's layer, under every circumstance, if you intend a mixed use like I do. Otherwise, you wind up with a jacked up layerBacked view.


the View maintains the view's layer all the time, after you set wantsLayer = true, the first time. So the only way to be certain you haven't broken anything, is to replace the backing layer. This is kind of the same thing as having a view hierarchy, and it has less over head. So, my question is answered.


in addition to that, I found that I shouldn't be getting an empty View.layer property. so I doubled down on my efforts to track down that bug.

it appears that the way I was handling the setup, and the un-setup of the View's layer was being called out of order. So, we very quickly got into a position where we are setting the view's layer before we've saved it. This was because I was relying on the properties' didSet subMethod to set the view back to default state, and I was instantiating the incoming controller As I was setting it... and the setup occurs at initialization. So my View's layer gets swapped out by the incoming controller's setup. the View notices it WILL be changing the controller so it tries to set the View to default, and it wipes out the new Controller's layer from the view. In the Next pass of changing the view's layer, it gets set to nil. I had to add print("") calls to every load/unload method in the app, before I saw that.