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

========

[あらゆるdelegateをBlockで実装するためのクラスの作り方](http://qiita.com/ryota-ku/items/8b6dcf1c545fa95b36c9)と[標準クラス(UIAlertView等)のdelegateをBlockで書く](http://qiita.com/ryota-ku/items/90f65a46336fcf686137)では各種delegate(Protocolを使ったcallback)をBlockで書く方法について書きました。この記事ではその他よく使うcallbackでBlockでは書けないものをBlockで書けるようにする方法を考えます。

これらの記事を前提としています。

* [クラスを継承せず既存クラスにオブジェクトを保持する機能を追加する](http://qiita.com/ryota-ku/items/3fe7218b93d8e2a61a2b)
* [Blockで動的にメソッドを実装できるクラスの作り方](http://qiita.com/ryota-ku/items/e674615a613061c08cae)
* [動的にサブクラスを登録しインスタンスを生成する](http://qiita.com/ryota-ku/items/45e89348974e39f4f3b9)

また全てのコードを記載していないので、完全に動くコードが必要な場合は[このへん](https://github.com/siagency/SIATools/tree/master/SIATools/Blocks)を探してください。


UIControl
---------

### 使い方

```objectivec
    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の引数として受け取ります


### インタフェース

```objectivec: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
```

### 実装

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

```objectivec: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の場合
------------------------

### 使い方

```objectivec
    UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] init];
    [recognizer sia_addActionWithUsingBlock:^{
        NSLog(@"%s", __PRETTY_FUNCTION__);
    }];
    recognizer.numberOfTapsRequired = 1;
    [self.view addGestureRecognizer:recognizer];
```

使い方のみ書きます。UIControlとほぼ同等の内容で実現できます。UIEventに当たるものはないので、Blockの引数はなしとしています。


KVOの場合
--------

### 使い方

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

### インタフェース

```objectivec: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
```

```objectivec: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の場合
------------

### 使い方

```objectivec
    __weak __block NSTimer *timer = [NSTimer sia_scheduledTimerWithTimeInterval:1 repeats:YES usingBlock:^{
        [timer invalidate];
    }];
```

NSTimerは保持する必要がない(内部で勝手にされる)ため__weakとしています。

またBlockが生成されるタイミングではまだ`timer`はnilです。そのタイミングでtimerをキャプチャしてもnilとなってしまうので__blockとしています。

### インタフェース

```objectivec: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
```

### 実装

```objectivec: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の場合
--------------------

### 使い方

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

### インタフェース

```objectivec: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
```

### 実装

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

```objectivec: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もどきの作り方](http://qiita.com/ryota-ku/items/76e050fd71e9b1be214e)の一部として書いています。興味のあるかたはこちらも御覧ください。
この記事の内容の他に、delegateのBlockでの実装等があります。