NSTimer Category + Blocks implementation to replace selector
I am quite new to blocks and objective-c, and i am trying to write my first category using both. My idea is to create a category on NSTimer that will receive a block as a parameter and this block will be used in the selector call. Right now I have this.
// NSTimer+Additions.h
#import <Foundation/Foundation.h>
typedef void (^VoidBlock)();
@interface NSTimer (NSTimer_Additions)
+ (NSTimer *)scheduleTimerWithTimeInterval:(NSTimeInterval)theSeconds repeats:(BOOL)repeats actions:(VoidBlock)actions;
@end
#import "NSTimer+Additions.h"
static VoidBlock _voidBlock;
@interface NSTimer (AdditionsPrivate) // Private stuff
- (void)theBlock;
@end
@imp开发者_C百科lementation NSTimer (NSTimer_Additions)
+ (NSTimer *)scheduleTimerWithTimeInterval:(NSTimeInterval)theSeconds repeats:(BOOL)repeats actions:(VoidBlock)actions {
[_voidBlock release];
_voidBlock = [actions copy];
NSTimer* timer = [[NSTimer alloc] initWithFireDate:[NSDate date]
interval:theSeconds
target:self
selector:@selector(theBlock)
userInfo:nil
repeats:repeats];
[timer fire];
return [timer autorelease];
}
- (void)theBlock {
_voidBlock();
}
@end
Gist for the code: https://gist.github.com/1065235
Everything compiles fine but i have the following error:
2011-07-05 14:35:47.068 TesteTimer[37716:903] * Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+[NSTimer theBlock]: unrecognized selector sent to class 0x7fff70bb0a18'
How can I make this category work?
Your major flaw besides the wrong target is your use of a static variable. You won't be able to support beyond a single timer.
Using block as argument to the invoked method.
@interface NSTimer (AdditionsPrivate) // Private stuff
- (void)theBlock:(VoidBlock)voidBlock;
@end
@implementation NSTimer (NSTimer_Additions)
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)theSeconds repeats:(BOOL)repeats actions:(VoidBlock)actions {
NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:[self instanceMethodSignatureForSelector:@selector(theBlock:)]];
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:theSeconds
invocation:invocation
repeats:repeats];
[invocation setTarget:timer];
[invocation setSelector:@selector(theBlock:)];
Block_copy(actions);
[invocation setArgument:&actions atIndex:2];
Block_release(actions);
return timer;
}
- (void)theBlock:(VoidBlock)voidBlock {
voidBlock();
}
@end
The problem with using associative references was the leak as there was no good point to release the block.
Earlier Approach using associative references
You can use associative references
to attach the block to that particular instance of NSTimer
.
@implementation NSTimer (NSTimer_Additions)
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)theSeconds repeats:(BOOL)repeats actions:(VoidBlock)actions {
NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:[self instanceMethodSignatureForSelector:@selector(theBlock)]];
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:theSeconds
invocation:invocation
repeats:repeats];
[invocation setTarget:timer];
[invocation setSelector:@selector(theBlock)];
objc_setAssociatedObject(timer, @"Block", actions, OBJC_ASSOCIATION_COPY);
return timer;
}
- (void)theBlock {
VoidBlock _voidBlock = (VoidBlock)objc_getAssociatedObject(self, @"Block");
_voidBlock();
}
@end
What about leveraging userInfo to carry your block? (this is done with ARC)
void (^callback)(void) = ^{
NSLog(@"do stuff");
}
NSTimer *timer = [NSTimer timerWithTimeInterval:10.0 target:self selector:@selector(handleTimeout:) userInfo:[NSDictionary dictionaryWithObject:[callback copy] forKey:@"block"] repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
And then add the static selector of:
+ (void)handleTimeout:(NSTimer *)timer
{
void (^callback)(void) = [timer.userInfo objectForKey:@"block"];
callback();
[timer invalidate];
timer = nil;
};
This should work:
NSTimer* timer = [[NSTimer alloc] initWithFireDate:[NSDate date]
interval:theSeconds
target:timer
selector:@selector(theBlock)
userInfo:nil
repeats:repeats];
The problem is that you're setting the target of the new NSTimer
instance to be self
. However, in the context of + scheduleTimerWithTimeInterval:repeats:actions:
(notice the +
), self
is NSTimer
, and not (as you probably thought) your newly-created NSTimer
instance.
As you can see from the error message, your app is crashing because NSTimer
doesn't respond to the class method + theBlock
, which is of course correct since you only defined the instance method - theBlock
.
精彩评论