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
#implementation 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.
Related
I am trying to make a timer app. I am fine with Play button but I couldnt get Pause button working. I have seen some tutorials on Timer Apps and most of them have only used: [timer invalidate] code for that method that solely stops the time that is currently being shown in the label (display). Even that code doesn't work for me so I tried doing this which makes kinda more sense but still, of no luck.
#implementation ViewController
int timerCounter=0;
NSTimer *timer;
NSString *label;
BOOL isPaused=NO;
-(IB Action) playButton:(id)sender{
[timer invalidate];
isPaused=NO;
timer=[NSTimer scheduledTimerWithTimeInterval:1 target:self selector: #selector(tick) userInfo:nil repeats:YES];
}
-(IBAction) pauseButton:(id)sender{
[timer invalidate];
isPaused=YES;
label=[NSString stringWithFormat:#"%d",timerCounter];
_labelTimer.text=label;
}
-(void) tick{
if (isPaused==NO){
timerCounter++;
}
label=[NSString stringWithFormat:#"%d",timerCounter];
_labelTimer.text=label;
}
The NSTimer API do not have any method for pausing. What is available is either fire or invalidate. About your code, You are using global variables - not a good practice, most probably the instance of timer you are calling is not the same, remove them and add a property in the class extension in .m instead:
#property (nonatomic, strong) NSTimer * timer;
you then address that property with self.timer.
If this does not help, check if the button call the method when you press it.
I am working on Blocks programming, i have created a block.
void(^paint)(void)=^(void){
NSLog(#"Process ");
};
now i want to create a nstimer using the NSinvocation and NSMethodSignatiureas below.
void(^startPainting)(id)=^(id self){
**SEL selectorToCall=#selector(paint());**
NSMethodSignature *methodSignature=[[self class] instanceMethodSignatureForSelector:selectorToCall];
NSInvocation *invocation=[NSInvocation invocationWithMethodSignature:methodSignature];
[invocation setSelector:selectorToCall];
[invocation setTarget:self];
[self setPaintTimer:[NSTimer scheduledTimerWithTimeInterval:1.0 invocation:invocation repeats:YES] ];
// self.paintTimer=[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(paint:) userInfo:nil repeats:YES];
};
The problem here is i want to send and the block paint to SEL.As the current statement is giving error.Please suggest me where i am doing wrong.How i can pass this block to this selector ?
SEL selectorToCall=#selector(paint());
You could use a dispatch-queues instead of selectors, works the same way and much better than the delay caused by firing a selector:
dispatch_async(dispatch_get_main_queue(), paint);
Regards
Heider
In my app, I associate an NSTimer with a block passed to a method; the block is also added to a an array of blocks. When the timer fires, its associated block is called and should be removed from the array. So my setup looks like this:
#interface MyObject : NSObject
#property(strong, nonatomic) NSMutableArray *allBlocks;
- (void)myMethodWithBlock:(void(^)(void))block;
- (void)timerFired:(NSTimer *)timer;
#end
#implementation MyObject
- (id)init
{
self = [super init];
if (self)
{
self.allBlocks = [NSMutableArray array];
}
return self;
}
- (void)myMethodWithBlock:(void(^)(void))block
{
[NSTimer scheduledTimerWithTimeInterval:5.0f
target:self
selector:#selector(timerFired:)
userInfo:block
repeats:NO];
[self.allBlocks addObject:block];
}
- (void)timerFired:(NSTimer *)timer
{
void(^block)(void) = timer.userInfo;
[self.allBlocks removeObject:block];
block();
}
#end
My problem is that when timerFired: is called, the block is (sometimes) not removed. Why?
The problem here is that NSTimer copies the block assigned to userInfo, but the block passed to myMethodWithBlock: is probably an instance of NSStackBlock, which is not equal to its copies.
Let's consider three scenarios, where myObject is an instance of MyObject:
// A
void(^myBlock)(void) = ^{
NSLog(#"1");
};
[myObject myMethodWithBlock:myBlock];
// B
int one = 1;
void(^myBlock)(void) = ^{
NSLog(#"%d", one);
};
[myObject myMethodWithBlock:myBlock];
// C
int one = 1;
[myObject myMethodWithBlock:^{
NSLog(#"%d", one);
};];
In A, the block captures no variables from its context; the block will be an instance of NSGlobalBlock, which simply returns itself when copied.
In B, the block captures the variable one; it will be an instance of NSMallocBlock, which also returns itself when copied.
In C, the block again captures the variable one, but is also not assigned to a variable before being passed to myMethodWithBlock:. In this case, the block is an instance of NSStackBlock, which returns an instance of NSMallocBlock when copied.
The result of this is that in a situation like scenario C, the NSStackBlock will be added to allBlocks, while an NSMallocBlock will be assigned to the timer's userInfo. When the timer fires, removeObject: does nothing since the block assigned to the timer isn't equal to any of the blocks in the array.
The solution is to always copy the block before storing it in the array. This way, the same block instance will be stored in the array and assigned to the timer:
- (void)myMethodWithBlock:(void(^)(void))block
{
block = [block copy];
[NSTimer scheduledTimerWithTimeInterval:5.0f
target:self
selector:#selector(timerFired:)
userInfo:block
repeats:NO];
[self.allBlocks addObject:block];
}
A clearer approach is to mark the blocks with something whose isEqual: behavior is well known and more readable, like an NSNumber...
// keep state so these can be made unique
#property(nonatomic, assign) NSInteger blockIndex;
// change the blocks collection to record blocks' associations with numbers
#property(nonatomic, strong) NSMutableDictionary *allBlocks;
// in myMethod...
NSNumber *nextIndex = [NSNumber numberWithInt:++self.blockIndex];
self.allBlocks[nextIndex] = block;
// pass userInfo:nextIndex when you schedule the timer
Now the timer context never has a block, copied or otherwise. Then, when the timer fires...
- (void)timerFired:(NSTimer *)timer {
NSNumber *index = timer.userInfo;
void(^block)(void) = self.allBlocks[index];
[self.allBlocks removeObjectForKey:index];
block();
}
I think it will be safest to compare apples to apples.
- (void)myMethodWithBlock:(void(^)(void))block
{
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:5.0f
target:self
selector:#selector(timerFired:)
userInfo:block
repeats:NO];
[self.allBlocks addObject:timer.userInfo];
}
- (void)timerFired:(NSTimer *)timer
{
[self.allBlocks removeObject:timer.userInfo];
void(^block)(void) = timer.userInfo;
block();
}
In my viewController, I have a button that calls a method to start a timer in a Timer class. Just like this
main view controller
[self.timer startTimer];
In the timer class, startTimer calls a countdownTime method, both of which you can see below. Very simple. At the end of the countdownTime, method, the time is put in the label in the view like this as I iterate through a loop of the clocks.
Timer *timerblah = self.gameClocks[i];
[self.gameClocks[i] setText: [NSString stringWithFormat:#"%#", self.time]];
In other words, the Timer class has an array property that holds all the clocks and that is connected to the view, hence the ability to set the text.
The problem with this code is that the model (i.e. Timer class) is setting the text in the view. I want the view Controller to get the time from the model and have the text set in the view controller i.e. have the viewController and only the viewController communicate with the view. It is no problem for me to get the array of clocks in the viewController, however, I'm sure how to pass the time back from the countdownTime method to the viewController. It would seem like overkill to set up an NSNotification class to send the time back every second, wouldn't it?
-(void)startTimer{
self.NStimer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:#selector(countdownTime:)
userInfo:nil
repeats:YES];
}
-(void)countdownTime:(NSTimer *)timer
{
#logic to countdown time
self.minutes = self.secondsRemaining / 60;
self.stringMinutes = [NSString stringWithFormat:#"%i", self.minutes];
self.seconds = self.secondsRemaining - (self.minutes * 60);
self.stringSeconds = [NSString stringWithFormat:#"%i", self.seconds];
if (self.seconds < 10) self.stringSeconds = [NSString stringWithFormat:#"0%i", self.seconds];
self.time = [NSString stringWithFormat:#"%#:%#", self.stringMinutes, self.stringSeconds];
self.secondsRemaining -= 1;
if (self.secondsRemaining == 0){
[self stopTimer];
}
#adding to the view (from the model i.e. in Timer.m class)
for(int i = 0; i < [self.gameClocks count]; i++){
Timer *timerblah = self.gameClocks[i];
if (timerblah.systemClock == YES){
[self.gameClocks[i] setText: [NSString stringWithFormat:#"%#", self.time]];
}
}
}
Update
There's a comment suggesting that I use a block to do this. In the timer class, I added a method like this to return a string with the time (the time is set to self.time above)
-(NSString *)passTheTime:(NSString (^)(void))returnBlock
{
return self.time;
}
I then, in view controller, call the method passTheTime on the timer class. I created a property in the view controller that will store the time, so I pass that as a parameter to the block,
[self.timer passTheFoo:^NSString * (self.timeToSet){
}];
a) I'm unsure of what to do in the block here.
b) I'm unsure of whether it was necessary to pass self.timeToSet into the block. How do I connect self.time from the Timer class to self.timeToSet in the view controller
c) there's an error incompatible block pointer type sending NSString *((^)(void)) to parameter of type NSString(^)(void)
d) alternatively, can I pass a block to startTimer, and have that block passed in the selector countdownTime and return the self.time once the calculations in countdownTime are finished?
You can use block to update the label in the view controller class as below
In Timer Class, create a block
TimerClass.h
#import <Foundation/Foundation.h>
typedef void(^TimerCallback)(NSString *time);
#interface TimerClass : NSObject{
TimerCallback timerCallback;
NSTimer *timer;
}
- (void)startTimerWithCallBack:(TimerCallback)callback;
#end
In TimerClass.m , update the code as below
#import "TimerClass.h"
#implementation TimerClass
- (void)startTimerWithCallBack:(TimerCallback)callback{
timerCallback = callback;
timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:#selector(countdownTime:)
userInfo:nil
repeats:YES];
}
-(void)countdownTime:(NSTimer *)timer
{
// countdown logic here
// call the callback method with time as a parameter to the viewcontroller class
// here for demo purpose I am passing random number as a value in callback, you need to pass the time that you want to display here
NSString *str = [NSString stringWithFormat:#"%d",1+arc4random()%10];
timerCallback(str);
}
#end
In ViewController class, call the timer start method as follows
TimerClass *timer = [[TimerClass alloc] init];
[timer startTimerWithCallBack:^(NSString *time) {
lbl.text = time; // display time in label
}];
I am having trouble with NSTimer. The below code is called from a method and it creates an NSTimer and sets it to the instance variable self.syncDelay. The if statement at the end of the method checking if the timer is valid gets called.
However when I call this method again before the timer runs out, the if statement at the top of the method doesn't get called. Which, I am guessing, is why [self.syncDelay invalidate]; doesn't invalidate the timer.
I have a property defined as: #property (nonatomic, strong) NSTimer *syncDelay;
The functionality I am looking for is the timer being invalidated when the method is called for the second time before the timer runs out and creating a new timer. Thus effectively resetting the timer.
EDIT: When the method gets called for the second the NSTimer is nil. Which is strange as I am keeping a strong pointer to it.
else {
if ([self.syncDelay isValid]) {
NSLog(#"Timer valid at start of method");
}
[self.syncDelay invalidate];
self.syncDelay = nil;
NSInvocation *timerInvocation = [NSInvocation invocationWithMethodSignature:
[self methodSignatureForSelector:#selector(testMethod:)]];
// configure invocation
[timerInvocation setSelector:#selector(testMethod:)];
[timerInvocation setTarget:self];
[timerInvocation setArgument:&className atIndex:2]; // argument indexing is offset by 2 hidden args
self.syncDelay = [NSTimer scheduledTimerWithTimeInterval:10
invocation:timerInvocation
repeats:NO];
if ([self.syncDelay isValid]) {
NSLog(#"Timer valid at end of method");
}
}