Using CALayer Delegate

2019-01-10 07:13发布

问题:

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.

回答1:

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).



回答2:

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;


回答3:

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.



回答4:

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.



回答5:

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];

}


回答6:

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.



回答7:

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



回答8:

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.