Posted at

callbackをBlockで書く(UIControl, UIGestureRecognizer, KVO, NSTimer の場合)

More than 5 years have passed since last update.

========

あらゆる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の引数として受け取ります


インタフェース


UIControl+SIABlocks.h

@class SIAControlAction;

@interface UIControl (SIABlocks)

- (SIAControlAction *)sia_addActionForControlEvents:(UIControlEvents)controlEvents
usingBlock:(void(^) (UIEvent * event))block;
- (void)sia_removeAction:(SIAControlAction *)action forControlEvents:(UIControlEvents)controlEvents;

@end



実装


UIControl+SIABlocks.m

#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を呼ぶだけです。


UIControl+SIABlocks.m

@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等はほぼメインスレッドから実行されるのがわかっている)


インタフェース


NSObject+SIABlocksKVO.h

@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



NSObject+SIABlocksKVO.m

#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としています。


インタフェース


NSTimer+SIABlocks.h

@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



実装


SIATimerAction.m

#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を管理させるようなコードとなっています。


インタフェース


NSObject+SIABlocksTargetAction.h

@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



実装


NSObject+SIABlocksTargetAction.m

#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の機能でメソッドを実装します。


NSObject+SIABlocksTargetAction.m

@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での実装等があります。