LoginSignup
19
19

More than 5 years have passed since last update.

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

Posted at

========

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

19
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
19