36

I have a UIView whose layers will have sublayers. I'd like to assign delegates for each of those sublayers, so the delegate method can tell the layer what to draw. My question is:

What should I provide as CALayer's delegate? The documentation says not to use the UIView the layers reside in, as this is reserved for the main CALayer of the view. But, creating another class just to be the delegate of the CALayers I create defeats the purpose of not subclassing CALayer. What are people typically using as the delegate for CALayer? Or should I just subclass?

Also, why is it that the class implementing the delegate methods doesn't have to conform to some sort of CALayer protocol? That's a wider overarching question I don't quite understand. I thought all classes requiring implementation of delegate methods required a protocol specification for implementers to conform to.

Shaun Budhram
  • 3,690
  • 4
  • 30
  • 41
  • 1
    I can confirm that using a containing view as the delegate crashes the app, without a stack trace, even if I don't implement any of the delegate methods. – Felixyz Mar 18 '10 at 19:32
  • Did you ever get this to work? – haroldcampbell Jan 19 '11 at 02:42
  • 1
    I ended up creating a class that subclasses NSObject, that resides in the same file as the view, named something like LayerDelegate. The purpose of this class is specifically to handle the delegate callback. It's easy enough to maintain since they're in the same file. This class's only method is drawLayer:inContext: and handles the drawing. – Shaun Budhram Jan 20 '11 at 01:31

8 Answers8

31

Preferring to keep the layer delegate methods in my UIView subclass, I use a basic re-delegating delegate class. This class can be reused without customization, avoiding the need to subclass CALayer or create a separate delegate class just for layer drawing.

@interface LayerDelegate : NSObject
- (id)initWithView:(UIView *)view;
@end

with this implementation:

@interface LayerDelegate ()
@property (nonatomic, weak) UIView *view;
@end

@implementation LayerDelegate

- (id)initWithView:(UIView *)view {
    self = [super init];
    if (self != nil) {
        _view = view;
    }
    return self;
}

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)context {
    NSString *methodName = [NSString stringWithFormat:@"draw%@Layer:inContext:", layer.name];
    SEL selector = NSSelectorFromString(methodName);
    if ([self.view respondsToSelector:selector] == NO) {
        selector = @selector(drawLayer:inContext:);
    }

    void (*drawLayer)(UIView *, SEL, CALayer *, CGContextRef) = (__typeof__(drawLayer))objc_msgSend;
    drawLayer(self.view, selector, layer, context);
}

@end

The layer name is used to allow for per-layer custom draw methods. For example, if you have assigned a name to your layer, say layer.name = @"Background";, then you can implement a method like this:

- (void)drawBackgroundLayer:(CALayer *)layer inContext:(CGContextRef)context;

Note, your view will need a strong reference the instance of this class, and it can be used as the delegate for any number of layers.

layerDelegate = [[LayerDelegate alloc] initWithView:self];
layer1.delegate = layerDelegate;
layer2.delegate = layerDelegate;
Dave Lee
  • 6,299
  • 1
  • 36
  • 36
28

The lightest-wight solution would be to create a small helper class in the the file as the UIView that's using the CALayer:

In MyView.h

@interface MyLayerDelegate : NSObject
. . .
@end

In MyView.m

@implementation MyLayerDelegate
- (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)ctx
{
. . .
}
@end

Just place those at the top of your file, immediately below the #import directives. That way it feels more like using a "private class" to handle the drawing (although it isn't -- the delegate class can be instantiated by any code that imports the header).

Felixyz
  • 19,053
  • 14
  • 65
  • 60
  • 8
    You can put the `@interface` inside of the .m file for an even better _private class_ feel. If you need to refer to the class in the .h file (e.g. for an ivar), you can use forward declarations like `@class MyLayerDelegate;`. – Daniel Dickison Nov 04 '10 at 14:44
  • This answer is great, however, you might want to just go ahead and make this class a subclass of `CALayer` and draw there if you have a single layer. – Mazyod Nov 22 '12 at 13:31
  • Note that in Swift, it seems the drawLayer:inContext: definition should be an override, otherwise it won't be recognized as implementing the informal delegate protocol. Subclassing CALayer (or even NSObject) works. – MaryAnn Mierau May 21 '15 at 23:48
10

Take a look at the docs on formal vs informal protocols. The CALayer is implementing an informal protocol which means that you can set any object to be its delegate and it will determine if it can send messages to that delegate by checking the delegate for a particular selector (i.e. -respondsToSelector).

I typically use my view controller as the delegate for the layer in question.

Matt Long
  • 24,438
  • 4
  • 73
  • 99
3

A note regarding "helper" classes for use as a layer's delegate (with ARC at least):

Make sure you store a "strong" reference to your alloc/init'd helper class (such as in a property). Simply assigning the alloc/init'd helper class to the delegate seems to cause crashes for me, presumably because mylayer.delegate is a weak reference to your helper class (as most delegates are), so the helper class gets freed up before the layer can use it.

If I assign the helper class to a property, then assign it to the delegate, my weird crashes go away, and things behave as expected.

Dale
  • 133
  • 1
  • 7
  • Ahha. I've been wondering why I'm getting crashes in my code. I'm using the CALayer delegate to provide a connection to a business logic class which is backed by the layer. In debug mode and on the simulator, it runs fine, but in release mode on the device I'm getting all kinds of crashes. If CALayer.delegate is a weak reference, then that would explain it. – Dr Joe Mar 17 '13 at 13:59
2

I personally voted for Dave Lee's solution above as being the most encapsulating, particularly where you have multiple layers. However; when I tried it on IOS 6 with ARC I got errors on this line and suggesting that I need a bridged cast

// [_view performSelector: selector withObject: layer withObject: (id)context];

I therefore amended Dave Lee's drawLayer method from his re-delegating delegate class to employ NSInvocation as below. All usage and ancillary functions are identical to those Dave Lee posted on his earlier excellent suggestion.

-(void) drawLayer: (CALayer*) layer inContext: (CGContextRef) context
{
    NSString* methodName = [NSString stringWithFormat: @"draw%@Layer:inContext:", layer.name];
    SEL selector = NSSelectorFromString(methodName);

    if ( ![ _view respondsToSelector: selector])
    {
        selector = @selector(drawLayer:inContext:);   
    }

    NSMethodSignature * signature = [[_view class] instanceMethodSignatureForSelector:selector];
    NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:signature];

    [invocation setTarget:_view];             // Actually index 0    
    [invocation setSelector:selector];        // Actually index 1    

    [invocation setArgument:&layer atIndex:2];
    [invocation setArgument:&context atIndex:3];

    [invocation invoke];

}
Nianliang
  • 2,926
  • 3
  • 29
  • 22
James
  • 184
  • 1
  • 5
0

I prefer the following solution. I would like to use the drawLayer:inContext: method of the UIView to render a subview that I might add without adding extra classes all over the place. My solution is as follows:

Add the following files to your project:

UIView+UIView_LayerAdditions.h with contents:

@interface UIView (UIView_LayerAdditions)

- (CALayer *)createSublayer;

@end

UIView+UIView_LayerAdditions.m with contents

#import "UIView+UIView_LayerAdditions.h"

static int LayerDelegateDirectorKey;

@interface LayerDelegateDirector: NSObject{ @public UIView *view; } @end
@implementation LayerDelegateDirector

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
    [view drawLayer:layer inContext:ctx];
}

@end

@implementation UIView (UIView_LayerAdditions)

- (LayerDelegateDirector *)director
{
    LayerDelegateDirector *director = objc_getAssociatedObject(self, &LayerDelegateDirectorKey);
    if (director == nil) {
        director = [LayerDelegateDirector new];
        director->view = self;
        objc_setAssociatedObject(self, &LayerDelegateDirectorKey, director, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    return director;
}

- (CALayer *)createSublayer
{
    CALayer *layer = [CALayer new];
    layer.contentsScale = [UIScreen mainScreen].scale;
    layer.delegate = [self director];
    [self.layer addSublayer:layer];
    [layer setNeedsDisplay];
    return layer;
}

@end

Now add the header to your .pch file. If you add a layer using the createSublayer method, it will automagically show up without bad allocs in the override to drawLayer:inContext:. As far as I know the overhead of this solution is minimal.

Gleno
  • 16,621
  • 12
  • 64
  • 85
0

It's possible to implement a delegation without resorting to a strong ref.

NOTE: The basic concept is that you forward the delegate call to a selector call

  1. Create a selector instance in the NSView you want to get the delegation from
  2. implement the drawLayer(layer,ctx) in the NSView you want to get the delegation from call the selector variable with the layer and ctx vars
  3. set the view.selector to a handleSelector method where you then retrieve the layer and ctx (this can be anywhere in your code, weak or strongly referenced)

To see an example of how you implement the selector construction:(Permalink) https://github.com/eonist/Element/wiki/Progress#selectors-in-swift

NOTE: why are we doing this? because creating a variable outside methods whenever you want to use the Graphic class is non-sensical

NOTE: And you also get the benefit that the receiver of the delegation doesn't need to extend NSView or NSObject

Sentry.co
  • 5,355
  • 43
  • 38
-2

Can you use the passed in layer parameter to construct a switch statement so you can put everything in this method(against the advice of the documents):

-(void) drawLayer: (CALayer*) layer inContext: (CGContextRef) context {
   if layer = xLayer {...}
 }

Just my 2 cents.

randomor
  • 5,329
  • 4
  • 46
  • 68