5

I am making a custom NSView object that has some content that changes often, and some that changes much more infrequently. As it would turn out, the parts that change less often take the most time to draw. What I would like to do is render these two parts in different layers, so that I can update one or the other separately, thus sparing my user a sluggish user interface.

How might I go about doing this? I have not found many good tutorials on this sort of thing, and none that talk about rendering NSBezierPaths on a CALayer. Ideas anyone?

mtmurdock
  • 12,756
  • 21
  • 65
  • 108

1 Answers1

4

Your hunch is right, this is actually an excellent way to optimise drawing. I've done it myself where I had some large static backgrounds that I wanted to avoid redrawing when elements moved on top.

All you need to do is add CALayer objects for each of the content items in your view. To draw the layers, you should set your view as the delegate for each layer and then implement the drawLayer:inContext: method.

In that method you just draw the content of each layer:

- (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)ctx
{
    if(layer == yourBackgroundLayer)
    {   
        //draw your background content in the context
        //you can either use Quartz drawing directly in the CGContextRef,
        //or if you want to use the Cocoa drawing objects you can do this:
        NSGraphicsContext* drawingContext = [NSGraphicsContext graphicsContextWithGraphicsPort:ctx flipped:YES];
        NSGraphicsContext* previousContext = [NSGraphicsContext currentContext];
        [NSGraphicsContext setCurrentContext:drawingContext];
        [NSGraphicsContext saveGraphicsState];
        //draw some stuff with NSBezierPath etc
        [NSGraphicsContext restoreGraphicsState];
        [NSGraphicsContext setCurrentContext:previousContext];
    }
    else if (layer == someOtherLayer)
    {
        //draw other layer
    }
    //etc etc
}

When you want to update the content of one of the layers, just call [yourLayer setNeedsDisplay]. This will then call the delegate method above to provide the updated content of the layer.

Note that by default, when you change the layer content, Core Animation provides a nice fade transition for the new content. However, if you're handling the drawing yourself you probably don't want this, so in order to prevent the default fade in animation when the layer content changes, you also have to implement the actionForLayer:forKey: delegate method and prevent the animation by returning a null action:

- (id<CAAction>)actionForLayer:(CALayer*)layer forKey:(NSString*)key 
{
    if(layer == someLayer)
    {
        //we don't want to animate new content in and out
        if([key isEqualToString:@"contents"])
        {
            return (id<CAAction>)[NSNull null];
        }
    }
    //the default action for everything else
    return nil;
}
Rob Keniger
  • 45,830
  • 6
  • 101
  • 134
  • Thanks, this is very helpful. I am however having trouble getting the delegate method to fire. I am setting my view as the delegate and I am adding the layers as sublayers of the root layer, and I am calling `setNeedsDisplay` but `drawLayer:inContext` never gets called. Any ideas? – mtmurdock Feb 21 '12 at 17:32
  • I am also overriding `drawRect:` could that be causing it to not call the other function? – mtmurdock Feb 23 '12 at 23:14
  • 1
    Yep. You can't have both `drawRect:` and layer-based drawing in a view. You need to make sure you've called `setWantsLayer:YES` on the view also, otherwise the layers will be ignored. – Rob Keniger Feb 23 '12 at 23:24
  • when do you call `setWantsLayer:`? just once in constructor? Or every time you draw? – mtmurdock Feb 23 '12 at 23:33
  • Ok i've got it displaying now. I had to set the position, and the bounds to get it to show up, but its not rendering where I would expect. Everything is dropped most of the way off the view in the bottom right corner. Do CALayers use a different coordinate system than views? Are positions relative to the owning view or to the window? – mtmurdock Feb 23 '12 at 23:39
  • 1
    If the view is set up in Interface Builder then you can turn it on in the Core Animation inspector for the view (tick the box next to the view in the "Core Animation Layer" section). Otherwise, you can turn it on in the `initWithFrame:` method (if you're creating the layer programmatically) or in `initWithCoder:` if you're loading the view from a nib. – Rob Keniger Feb 23 '12 at 23:41
  • I don't understand. What am I turning on, and what does it do? – mtmurdock Feb 23 '12 at 23:43
  • 2
    CALayers use a different way of specifying coordinates. Have a look at [the docs](https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/CoreAnimation_guide/Articles/Layers.html#//apple_ref/doc/uid/TP40006082-SW8) – Rob Keniger Feb 23 '12 at 23:43
  • I was referring to `setWantsLayer`. – Rob Keniger Feb 23 '12 at 23:44