レイヤーにサブレイヤーがあるUIViewがあります。これらのサブレイヤーごとにデリゲートを割り当てて、デリゲートメソッドがレイヤーに何を描画するかを指示できるようにします。私の質問は:
CALayerのデリゲートとして何を提供する必要がありますか?ドキュメントには、レイヤーが存在するUIViewを使用しないように記載されています。これは、ビューのメインCALayer用に予約されているためです。しかし、私が作成したCALayerのデリゲートになるためだけに別のクラスを作成すると、CALayerをサブクラス化しないという目的が無効になります。 CALayerのデリゲートとして通常使用しているのは何ですか?それともサブクラスにする必要がありますか?
また、デリゲートメソッドを実装するクラスが何らかのCALayerプロトコルに準拠する必要がないのはなぜですか?それは私が完全には理解していないより広い包括的な質問です。デリゲートメソッドの実装を必要とするすべてのクラスには、実装者が準拠するためのプロトコル仕様が必要だと思いました。
最も軽量な解決策は、CALayerを使用するUIViewとしてファイルに小さなヘルパークラスを作成することです。
MyView.hで
@interface MyLayerDelegate : NSObject
. . .
@end
MyView.mで
@implementation MyLayerDelegate
- (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)ctx
{
. . .
}
@end
それらをファイルの先頭、#importディレクティブのすぐ下に配置するだけです。そうすれば、「プライベートクラス」を使用して描画を処理するように感じられます(ただし、そうではありません。デリゲートクラスは、ヘッダーをインポートする任意のコードでインスタンス化できます)。
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;
公式プロトコルと非公式プロトコルに関するドキュメント をご覧ください。 CALayerは非公式のプロトコルを実装しています。つまり、任意のオブジェクトをそのデリゲートに設定でき、特定のセレクター(つまり、-respondsToSelector)のデリゲートをチェックすることにより、そのデリゲートにメッセージを送信できるかどうかを判断します。
私は通常、問題のレイヤーのデリゲートとしてビューコントローラーを使用します。
レイヤーのデリゲートとして使用するための「ヘルパー」クラスに関する注意(少なくともARCを使用):
Alloc/init'dヘルパークラスへの「強力な」参照(プロパティなど)を必ず保存してください。デリゲートにalloc/init'dヘルパークラスを割り当てるだけでクラッシュが発生するようです。おそらくmylayer.delegateはヘルパークラスへの弱い参照であるため(ほとんどのデリゲートがそうであるように)、ヘルパークラスはレイヤーの前に解放されます。使用できます。
ヘルパークラスをプロパティに割り当ててからデリゲートに割り当てると、奇妙なクラッシュがなくなり、期待どおりに動作します。
私は個人的に、特に複数のレイヤーがある場合に、最もカプセル化されているものとして、上記の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];
}
私は次の解決策を好みます。 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:
へのオーバーライドに不正な割り当てがなくても自動的に表示されます。私の知る限り、このソリューションのオーバーヘッドは最小限です。
注:基本的な概念は、デリゲート呼び出しをセレクター呼び出しに転送することです。
セレクター構造の実装方法の例を見るには:(パーマリンク) https://github.com/eonist/Element/wiki/Progress#selectors-in-Swift
注:なぜこれを行うのですか? Graphicクラスを使用するときはいつでも、メソッドの外部で変数を作成することは無意味だからです。
注:また、委任の受信者がNSViewまたはNSObjectを拡張する必要がないという利点もあります。