Objective-C
Mac
iOS
Blocks

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

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