========
あらゆるdelegateをBlockで実装するためのクラスの作り方と標準クラス(UIAlertView等)のdelegateをBlockで書くでは各種delegate(Protocolを使ったcallback)をBlockで書く方法について書きました。この記事ではその他よく使うcallbackでBlockでは書けないものをBlockで書けるようにする方法を考えます。
これらの記事を前提としています。
また全てのコードを記載していないので、完全に動くコードが必要な場合はこのへんを探してください。
UIControl
使い方
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button sia_addActionForControlEvents:UIControlEventTouchUpInside usingBlock:^(UIEvent *event) {
NSLog(@"%s", __PRETTY_FUNCTION__);
}];
UIControlEventを指定して登録します。通常はbuttonのインスタンスが解放されればBlockも解放されますが、明示的に解除した場合はsia_addActionForControlEvents:usingBlock:
の戻り値を取っておきsia_removeAction:forControlEvents:
に渡して解除します。
UIControlのcallbackは引数が1つのaction:(id)sender
とふたつのaction:(id)sender forEvent:(UIEvent *)event
があります。senderは不要なのでUIEventのみをBlockの引数として受け取ります
インタフェース
@class SIAControlAction;
@interface UIControl (SIABlocks)
- (SIAControlAction *)sia_addActionForControlEvents:(UIControlEvents)controlEvents
usingBlock:(void(^) (UIEvent * event))block;
- (void)sia_removeAction:(SIAControlAction *)action forControlEvents:(UIControlEvents)controlEvents;
@end
実装
#define SIA_CONTROL_ACTION_SEL @selector(action:forEvent:)
#define SIAControlActionListKey "SIAControlActionListKey"
@interface SIAControlAction : NSObject
@property (nonatomic, assign, readonly) UIControlEvents controlEvents;
@property (nonatomic, copy, readonly) void (^block)(UIEvent *event);
@end
@implementation SIAControlAction
- (id)initWithControlEvents:(UIControlEvents)controlEvents
usingBlock:(void (^)(UIEvent *event))block
{
self = [super init];
if (self) {
_controlEvents = controlEvents;
_block = block;
}
return self;
}
- (void)action:(UIControl *)sender forEvent:(UIEvent *)event
{
_block(event);
}
@end
SIAControlActionはUIControlからcallbackされるメソッドを実装し、そのメソッドからBlockを呼ぶだけです。
@implementation UIControl (SIABlocks)
- (NSMutableArray *)sia_controlActions
{
NSMutableArray *actions = nil;
@synchronized(self) {
actions = [self sia_associatedObjectForKey:SIAControlActionListKey];
if (actions == nil) {
actions = @[].mutableCopy;
[self sia_setAssociatedObject:actions forKey:SIAControlActionListKey];
}
}
return actions;
}
- (SIAControlAction *)sia_addActionForControlEvents:(UIControlEvents)controlEvents
usingBlock:(void (^)(UIEvent *event))block
{
SIAControlAction *action = [[SIAControlAction alloc] initWithControlEvents:controlEvents usingBlock:block];
[self addTarget:action action:SIA_CONTROL_ACTION_SEL forControlEvents:controlEvents];
[[self sia_controlActions] addObject:action];
return action;
}
- (void)sia_removeAction:(SIAControlAction *)action forControlEvents:(UIControlEvents)controlEvents
{
[self removeTarget:action action:SIA_CONTROL_ACTION_SEL forControlEvents:controlEvents];
[[self sia_controlActions] removeObject:action];
}
@end
sia_controlActions
はAssociatedObjectの機能を使いNSMutableArrayを管理します。
sia_addActionForControlEvents:usingBlock:
はSIAControlActionをUIControlのcallbackとして登録、sia_controlAction
に保持します。SIAControlActionはAssociatedObjectの機能でUIControlに保持させることになるため、UIControlを解放すればBlockも解放されます。
sia_removeAction:forControlEvents:
は登録の逆の操作をします。UIControlが消えるタイミングでBlockは解放されるのであまり必要になることはないです。
UIGestureRecognizerの場合
使い方
UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] init];
[recognizer sia_addActionWithUsingBlock:^{
NSLog(@"%s", __PRETTY_FUNCTION__);
}];
recognizer.numberOfTapsRequired = 1;
[self.view addGestureRecognizer:recognizer];
使い方のみ書きます。UIControlとほぼ同等の内容で実現できます。UIEventに当たるものはないので、Blockの引数はなしとしています。
KVOの場合
使い方
SIATest *test = [[SIATest alloc] init];
[test sia_addActionForKeyPath:@"count" options:NSKeyValueObservingOptionNew queue:nil usingBlock:^(NSDictionary *change) {
NSLog(@"%s", __PRETTY_FUNCTION__);
}];
通常の通知メソッド(通知をされるメソッド)は- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
です。
keyPathとobjectはBlockでは必要ありません。他のところで登録したKVOの通知を受け取ることはないためです。
contextにあたるものは用意していません。Block内から外のローカル変数にアクセスする等で同等のことが可能です。
queueはNSNotificationCenterのBlockのメソッドを参考に追加しています。なくてもいいかも…? nilであればKVOの通知元スレッドからそのままblockが実行されます。何らかのNSOperationQueueを指定しておくと、通知元スレッドではなくそのキューでblockが実行されます。UIControlやUIGestureRecognizerでも同等の実装はできるのですが、通知元スレッドがハッキリしないKVOにのみqueueの機能を追加しました。(UIControl等はほぼメインスレッドから実行されるのがわかっている)
インタフェース
@class SIAObserverAction;
@interface NSObject (SIABlocksKVO)
- (SIAObserverAction *)sia_addActionForKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
queue:(NSOperationQueue *)queue
usingBlock:(void(^) (NSDictionary * change))block;
- (void)sia_removeAction:(SIAObserverAction *)action
forKeyPath:(NSString *)keyPath;
@end
#define SIAObserverActionListKey "SIAObserverActionListKey"
@interface SIAObserverAction : NSObject
@property (nonatomic, copy, readonly) NSString *keyPath;
@property (nonatomic, assign, readonly) NSKeyValueObservingOptions options;
@property (nonatomic, weak, readonly) NSOperationQueue *queue;
@property (nonatomic, copy, readonly) void (^block)(NSDictionary *change);
@end
@implementation SIAObserverAction
- (id)initWithKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
queue:(NSOperationQueue *)queue
usingBlock:(void (^)(NSDictionary *change))block
{
self = [super init];
if (self) {
_keyPath = keyPath;
_options = options;
_queue = queue;
_block = block;
}
return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if (self.queue && self.queue != [NSOperationQueue currentQueue]) {
[self.queue addOperationWithBlock:^{
_block(change);
}];
}
else {
_block(change);
}
}
@end
@implementation NSObject (SIABlocksKVO)
- (NSMutableArray *)sia_observerActions
{
NSMutableArray *actions = nil;
@synchronized(self) {
actions = [self sia_associatedObjectForKey:SIAObserverActionListKey];
if (actions == nil) {
actions = @[].mutableCopy;
[self sia_setAssociatedObject:actions forKey:SIAObserverActionListKey];
}
}
return actions;
}
- (SIAObserverAction *)sia_addActionForKeyPath:(NSString *)keyPath
options:(NSKeyValueObservingOptions)options
queue:(NSOperationQueue *)queue
usingBlock:(void (^)(NSDictionary *change))block
{
SIAObserverAction *action = [[SIAObserverAction alloc] initWithKeyPath:keyPath
options:options
queue:queue
usingBlock:block];
[self addObserver:action forKeyPath:keyPath options:options context:nil];
[[self sia_observerActions] addObject:action];
return action;
}
- (void)sia_removeAction:(SIAObserverAction *)action
forKeyPath:(NSString *)keyPath
{
[self removeObserver:action forKeyPath:keyPath context:nil];
[[self sia_observerActions] removeObject:action];
}
@end
実装はUIControlの場合とだいたい同じです。
Blockを実行するところにNSOperationQueueの機能を追加しています。NSOperationQueueが指定されていて、且つ現在のNSOperationQueueと異なる場合は指定のNSOperationQueueでBlockを実行します。
NSTimerの場合
使い方
__weak __block NSTimer *timer = [NSTimer sia_scheduledTimerWithTimeInterval:1 repeats:YES usingBlock:^{
[timer invalidate];
}];
NSTimerは保持する必要がない(内部で勝手にされる)ため__weakとしています。
またBlockが生成されるタイミングではまだtimer
はnilです。そのタイミングでtimerをキャプチャしてもnilとなってしまうので__blockとしています。
インタフェース
@interface NSTimer (SIABlocks)
+ (NSTimer *)sia_timerWithTimeInterval:(NSTimeInterval)ti
repeats:(BOOL)yesOrNo
usingBlock:(void (^)())block;
+ (NSTimer *)sia_scheduledTimerWithTimeInterval:(NSTimeInterval)ti
repeats:(BOOL)yesOrNo
usingBlock:(void (^)())block;
@end
実装
#define SIA_TIMER_ACTION_SEL @selector(fire:)
#define SIATimerActionKey "SIATimerActionKey"
@interface SIATimerAction : NSObject
@property (nonatomic, copy, readonly) void (^block)();
@end
@implementation SIATimerAction
- (id)initWithBlock:(void (^)())block
{
self = [super init];
if (self) {
_block = block;
}
return self;
}
- (void)fire:(NSTimer *)timer
{
_block();
}
@end
@implementation NSTimer (SIABlocks)
+ (NSTimer *)sia_timerWithTimeInterval:(NSTimeInterval)ti
repeats:(BOOL)yesOrNo
usingBlock:(void (^)())block
{
SIATimerAction *action = [[SIATimerAction alloc] initWithBlock:block];
NSTimer *timer = [NSTimer timerWithTimeInterval:ti
target:action
selector:SIA_TIMER_ACTION_SEL
userInfo:nil
repeats:yesOrNo];
[timer sia_setAssociatedObject:action forKey:SIATimerActionKey];
return timer;
}
+ (NSTimer *)sia_scheduledTimerWithTimeInterval:(NSTimeInterval)ti
repeats:(BOOL)yesOrNo
usingBlock:(void (^)())block
{
NSTimer *timer = [NSTimer sia_timerWithTimeInterval:ti repeats:yesOrNo usingBlock:block];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
return timer;
}
@end
UIControlの場合とほぼ同じです。複数Blockが登録されることがないので少しシンプルです。またscheduled〜(自動でタイマーが開始される)も用意しています。
target/selectorの場合
使い方
UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithTitle:@"Title" style:UIBarButtonItemStylePlain target:nil action:NULL];
SIABlockAction *action = [item sia_actionForSelector:@selector(action:)
types:"v0@0:0@0"
usingBlock:^ void (UIBarButtonItem *sender)
{
NSLog(@"%s %@", __PRETTY_FUNCTION__, sender);
}];
item.target = action;
item.action = action.selector;
target/selectorのために一応用意しているのですが、あまり使い勝手はよくありません。まあ仕方がないかな? sia_actionForSelector...
の引数のtypesとBlockはそのtarget/selecterに必要な型で渡す必要があり、特に決まった型はないため、型情報を自動で計算する等が難しいためです。
上記ではUIBarButtonItemにtarget/actionを設定しています。UIBarButtonItemとcallbackの生存期間を同じにするため、itemにSIABlockActionを管理させるようなコードとなっています。
インタフェース
@interface SIABlockAction : SIAExpandableObject
@property (nonatomic, assign) SEL selector;
+ (SIABlockAction *)createSubclassInstanceForSelector:(SEL)selector;
- (void)addMethodWithTypes:(const char *)types block:(id)block;
@end
@interface NSObject (SIABlocksTargetAction)
- (SIABlockAction *)sia_actionForSelector:(SEL)selector types:(const char *)types usingBlock:(id)block;
- (void)sia_disposeAction:(SIABlockAction *)action;
@end
実装
#define SIABlockActionListKey "SIABlockActionListKey"
@implementation SIABlockAction
+ (SIABlockAction *)createSubclassInstanceForSelector:(SEL)selector
{
SIABlockAction *action = [SIABlockAction createSubclassInstance];
action.selector = selector;
return action;
}
- (void)addMethodWithTypes:(const char *)types block:(id)block
{
[self addMethodWithSelector:self.selector types:types block:block];
}
@end
SELをプロパティに保持しておいて、型情報とBlockを受け取りSIAExpandableObjectの機能でメソッドを実装します。
@implementation NSObject (SIABlocksTargetAction)
- (NSMutableArray *)sia_blockActions
{
NSMutableArray *actions = nil;
@synchronized(self) {
actions = [self sia_associatedObjectForKey:SIABlockActionListKey];
if (actions == nil) {
actions = @[].mutableCopy;
[self sia_setAssociatedObject:actions forKey:SIABlockActionListKey];
}
}
return actions;
}
- (SIABlockAction *)sia_actionForSelector:(SEL)selector types:(const char *)types usingBlock:(id)block
{
SIABlockAction *action = [SIABlockAction createSubclassInstanceForSelector:selector];
[action addMethodWithTypes:types block:block];
[[self sia_blockActions] addObject:action];
return action;
}
- (void)sia_disposeAction:(SIABlockAction *)action
{
[[self sia_blockActions] removeObject:action];
}
@end
sia_blockActions
はAssociatedObjectの機能を使いNSMutableArrayを管理します。
sia_actionForSelector:types:usingBlock:
はSIABlockActionを生成し動的なメソッド実装しsia_blockActions
に保持します。SIABlockActionはAssociatedObjectの機能で対象インスタンスに保持させることになるため、対象インスタンスを解放すればBlockも解放されます。
sia_disposeAction:
は登録の逆の操作をします。
参考
この記事はBlocksKitもどきの作り方の一部として書いています。興味のあるかたはこちらも御覧ください。
この記事の内容の他に、delegateのBlockでの実装等があります。