web-dev-qa-db-ja.com

CALayerデリゲートの使用

レイヤーにサブレイヤーがあるUIViewがあります。これらのサブレイヤーごとにデリゲートを割り当てて、デリゲートメソッドがレイヤーに何を描画するかを指示できるようにします。私の質問は:

CALayerのデリゲートとして何を提供する必要がありますか?ドキュメントには、レイヤーが存在するUIViewを使用しないように記載されています。これは、ビューのメインCALayer用に予約されているためです。しかし、私が作成したCALayerのデリゲートになるためだけに別のクラスを作成すると、CALayerをサブクラス化しないという目的が無効になります。 CALayerのデリゲートとして通常使用しているのは何ですか?それともサブクラスにする必要がありますか?

また、デリゲートメソッドを実装するクラスが何らかのCALayerプロトコルに準拠する必要がないのはなぜですか?それは私が完全には理解していないより広い包括的な質問です。デリゲートメソッドの実装を必要とするすべてのクラスには、実装者が準拠するためのプロトコル仕様が必要だと思いました。

34
Shaun Budhram

最も軽量な解決策は、CALayerを使用するUIViewとしてファイルに小さなヘルパークラスを作成することです。

MyView.hで

@interface MyLayerDelegate : NSObject
. . .
@end

MyView.mで

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

それらをファイルの先頭、#importディレクティブのすぐ下に配置するだけです。そうすれば、「プライベートクラス」を使用して描画を処理するように感じられます(ただし、そうではありません。デリゲートクラスは、ヘッダーをインポートする任意のコードでインスタンス化できます)。

28
Felixyz

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;
30
Dave Lee

公式プロトコルと非公式プロトコルに関するドキュメント をご覧ください。 CALayerは非公式のプロトコルを実装しています。つまり、任意のオブジェクトをそのデリゲートに設定でき、特定のセレクター(つまり、-respondsToSelector)のデリゲートをチェックすることにより、そのデリゲートにメッセージを送信できるかどうかを判断します。

私は通常、問題のレイヤーのデリゲートとしてビューコントローラーを使用します。

9
Matt Long

レイヤーのデリゲートとして使用するための「ヘルパー」クラスに関する注意(少なくともARCを使用):

Alloc/init'dヘルパークラスへの「強力な」参照(プロパティなど)を必ず保存してください。デリゲートにalloc/init'dヘルパークラスを割り当てるだけでクラッシュが発生するようです。おそらくmylayer.delegateはヘルパークラスへの弱い参照であるため(ほとんどのデリゲートがそうであるように)、ヘルパークラスはレイヤーの前に解放されます。使用できます。

ヘルパークラスをプロパティに割り当ててからデリゲートに割り当てると、奇妙なクラッシュがなくなり、期待どおりに動作します。

3
Dale

私は個人的に、特に複数のレイヤーがある場合に、最もカプセル化されているものとして、上記のDaveLeeのソリューションに投票しました。しかしながら; IOS 6 with ARCで試してみたところ、この行でエラーが発生し、ブリッジキャストが必要であることが示唆されました

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

したがって、Dave Leeの再委任デリゲートクラスからのdrawLayerメソッドを、以下のようにNSInvocationを使用するように修正しました。すべての使用法と補助機能は、DaveLeeが以前の優れた提案に投稿したものと同じです。

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

}
2
James

私は次の解決策を好みます。 UIViewのdrawLayer:inContext:メソッドを使用して、あちこちにクラスを追加せずに追加できるサブビューをレンダリングしたいと思います。私の解決策は次のとおりです。

次のファイルをプロジェクトに追加します。

IView + UIView_LayerAdditions.h内容:

@interface UIView (UIView_LayerAdditions)

- (CALayer *)createSublayer;

@end

IView + 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
Gleno

強力な参照に頼ることなく、委任を実装することは可能です。

注:基本的な概念は、デリゲート呼び出しをセレクター呼び出しに転送することです。

  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を拡張する必要がないという利点もあります。

0
eonist