使用CALayer代理

36

我有一个UIView,其图层将有子图层。 我想为每个子图层分配代理,以便代理方法可以告诉图层绘制什么。 我的问题是:

我应该提供什么作为CALayer的代理?文档说不要使用图层所在的UIView,因为这是保留给视图的主CALayer的。 但是,创建另一个类只是为了成为我创建的CALayers的代理,这违背了不对CALayer进行子类化的目的。人们通常使用什么作为CALayer的代理? 还是我应该子类化?

此外,实现代理方法的类为什么不必符合某种CALayer协议?这是一个更广泛的问题,我不太理解。 我以为所有需要实现代理方法的类都需要一个协议规范让实现者符合。


1
我可以确认使用包含视图作为委托会导致应用程序崩溃,即使我没有实现任何委托方法,也不会生成堆栈跟踪。 - Felixyz
你最终解决了这个问题吗? - haroldcampbell
1
我最终创建了一个继承自NSObject的类,它与视图位于同一文件中,命名为<UIView>LayerDelegate。这个类的目的是专门处理委托回调。由于它们在同一个文件中,因此很容易维护。这个类的唯一方法是drawLayer:inContext:,用于处理绘制。 - Shaun Budhram
8个回答

31

为了保持我的UIView子类中的层代理方法,我使用了一个基本的重新委托委托类。这个类可以被重复使用,避免了需要对CALayer进行子类化或创建一个专门用于层绘制的委托类的需要。

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

使用这个实现:

@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

图层名称用于允许每个图层具有自定义的绘制方法。例如,如果您为图层分配了一个名称,比如 layer.name = @"Background";,那么您可以实现这样一个方法:

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

注意,你的视图需要强引用此类的实例,并且它可以用作任意数量图层的代理。

layerDelegate = [[LayerDelegate alloc] initWithView:self];
layer1.delegate = layerDelegate;
layer2.delegate = layerDelegate;

聪明的想法,特别是如果你有一堆子图层。 - Hari Honor
非常适合可重用性。谢谢! - epologee

28
最轻量级的解决方案是在使用CALayer的UIView中创建一个小的辅助类,将其放置于同一个文件中:
在MyView.h中:
@interface MyLayerDelegate : NSObject
. . .
@end

在 MyView.m 文件中

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

只需将这些代码放在文件顶部,紧接着#import指令下面即可。这样会更像使用一个“私有类”来处理绘图(但实际上并不是——任何导入头文件的代码都可以实例化委托类)。


8
你可以将@interface放在.m文件中,这样可以更好地创建一个私有类。如果你需要在.h文件中引用这个类(例如,为了声明一个实例变量),你可以使用如@class MyLayerDelegate;的前向声明。 - Daniel Dickison
这个答案很好,但是如果你只有一个图层,你可能想直接将这个类作为CALayer的子类并在那里绘制。 - Mazyod
请注意,在Swift中,drawLayer:inContext:定义应该是一个重写,否则它将无法被识别为实现非正式委托协议。子类化CALayer(甚至NSObject)都可以起作用。 - MaryAnn Mierau

10

请查看关于正式和非正式协议的文档。CALayer实现了非正式协议,这意味着您可以将任何对象设置为其代理,并通过检查代理中是否有特定选择器(即-respondsToSelector)来确定是否可以向该代理发送消息。

我通常将我的视图控制器作为所讨论的层的代理。


3

关于作为图层代理的“助手”类(至少在ARC下):

确保您为分配的助手类(例如在属性中)存储“强”引用。仅将分配的助手类分配给代理似乎会导致崩溃,可能是因为mylayer.delegate是对您的助手类的弱引用(就像大多数委托一样),因此在图层可以使用它之前,助手类被释放。

如果我将助手类分配给属性,然后将其分配给代理,则我的奇怪崩溃消失了,并且事情表现如预期。


啊哈。我一直在想为什么我的代码会崩溃。我正在使用CALayer代理来提供与支持该层的业务逻辑类的连接。在调试模式和模拟器上,它运行良好,但在设备上的发布模式中,我遇到了各种崩溃。如果CALayer.delegate是一个弱引用,那就可以解释这个问题了。 - Dr Joe

2

我个人认为Dave Lee的解决方案最具概括性,特别是在有多个层级的情况下。然而,在IOS 6上使用ARC时,我在这一行代码上遇到了错误,并建议我需要一个桥接转换。

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

因此,我修改了Dave Lee的drawLayer方法,使用NSInvocation如下。所有用法和辅助函数与Dave Lee在他早期的优秀建议中发布的相同。
-(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];

}

0
我更喜欢以下解决方案。我想使用UIView的drawLayer:inContext:方法来渲染一个子视图,而不必在各个地方添加额外的类。我的解决方案如下:
将以下文件添加到您的项目中: UIView+UIView_LayerAdditions.h,其内容如下:
@interface UIView (UIView_LayerAdditions)

- (CALayer *)createSublayer;

@end

UIView+UIView_LayerAdditions.m 包含以下内容

#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

现在将头文件添加到您的.pch文件中。如果使用createSublayer方法添加图层,则它将自动显示,而不会在覆盖drawLayer:inContext:时出现错误分配内存。据我所知,此解决方案的开销是最小的。


0

可以实现委托而不必使用强引用。

注意:基本概念是将委托调用转发到选择器调用

  1. 在要从中获取委托的NSView中创建一个选择器实例
  2. 在要从中获取委托的NSView中实现drawLayer(layer,ctx),并使用layer和ctx变量调用选择器变量
  3. 将view.selector设置为handleSelector方法,在其中检索图层和ctx(这可以在代码中的任何位置进行,弱或强引用)

要查看如何实现选择器构造的示例:(永久链接)https://github.com/eonist/Element/wiki/Progress#selectors-in-swift

注意:我们为什么要这样做?因为在每次想要使用Graphic类时创建方法外的变量是没有意义的

注意:您还可以获得接收委托的好处,而无需扩展NSView或NSObject


-2

你能否使用传递的层参数构建一个switch语句,以便将所有内容放入此方法中(违反文档的建议):

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

这只是我的个人看法。


2
看起来你想要一个 ==,这样它就可以测试相等性而不是赋值。 - drewish

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接