Unable to cancel NSTimer that executes blocks - ios

I know that these NSTimer questions have come up numerous times, however since none seem to involve executing blocks that change the UI, I figured this is still an original question.
I have a subclass of UIButton that, for convenience sake (me, coming from an Android background), has an onClick and onHoldClick function. onClick simply takes a block and executes it in the selector that responds to UIControlEventTouchUpInside. The click function works great. For example:
[myButton setOnClick:^{
NSLog(#"clicked");
}];
The hold click functionality is not working so well.
[myButton setOnHoldClick:^{
NSLog(#"still holding click...");
}];
This listens for the UIControlEventTouchDown event, and performs a task after a delay:
- (void)clickDown:(id)sender
{
isClicked = YES;
[self performSelector:#selector(holdLoop:) withObject:nil afterDelay:delay];//For the sake of the example, delay is set to 0.5
}
The hold loop runs a repeated timer on another function, which handles the block execution (the timer variable is an NSTimer declared in the header file):
-(void)holdLoop:(id)sender
{
[self cancelTimers];
_timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:#selector(death:) userInfo:nil repeats:YES];
}
-(void)death:(id)_delay
{
if (isClicked)
{
_holdBlock();
}
else
{
[self cancelTimers];
}
}
The block that executes changes the value of a float, which is used to update the value of a label, which is then redrawn.
The first time the hold click event occurs, this works great. After that, it seems like timers don't get canceled, and new timers are still added. This is what my cancelTimers function looks like (calls here are retrieved from a collection of the other questions on this topic):
-(void)cancelTimers
{
[_timer invalidate];
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(death:) object:nil];
}
What am I doing wrong, and how do I fix it?
Edit
I do, in fact, already have the function that responds to touch up inside:
- (void)clickUp:(id)sender
{
isClicked = NO;
[self cancelTimers];
_clickBlock();
}
Furthermore, I have realized that the issue comes from an unhandled cancel event. Is there a reason why iOS would auto-cancel my long press?

Solved
Since the block redrew the UI, it was also redrawing the buttons (and resetting their functionality). This event was causing a cancel event to be called on the button - which was not handled. Adding the following:
[self addTarget:self action:#selector(cancelClick:) forControlEvents:UIControlEventTouchCancel];
[self addTarget:self action:#selector(cancelClick:) forControlEvents:UIControlEventTouchUpOutside];
-(void)cancelClick:(id)sender
{
isClicked = NO;
[self cancelTimers];
}
As well as reconsidering what changes are made in the block, has gotten me past this issue.

As I understood from the comments and the code, the clickDown: is called for UIControlEventTouchDown so isClicked is set to YES when the first time the button is touched down. You need to add a selector to the event UIControlEventTouchUpInside. It's called when the user lifts his finger while being iside the bound of the button. Inside that method, set isClicked to NO.

Related

Disable a button on a timer iOS

I am making a timer app on XCode 7 using Objective-C and I would like to disable the button that is connected to the action startCount while the following code is being executed
- (IBAction)startCount:(id)sender {
countInt = 0;
self.Label.text = [NSString stringWithFormat:#"%i", countInt];
timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(countTimer) userInfo:nil repeats:YES];
}
What line of code would I need to add to this to disable the button connected to the action startCount?
Thanks
You need to disable the sender via the enabled flag:
((UIButton*)sender).enabled = false;
Don't forget to re-enable the button after the timer finishes.
If the enabled state is NO, the control ignores touch events [...]
Alternatively to the cast I made in the above code: Change your method signature to take in a UIButton*, not just an id, that way you can make sure the cast will not fail. A slight variation of the cast would be to cast to UIControl* instead.
If I understand you correctly: to make your code easier to understand (and to avoid unpleasant race conditions if there are multiple buttons that might be tapped/disabled), I would suggest avoiding using a timer. Instead, consider dispatch_after() like this:
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
sender.userInteractionEnabled = false;
});
We get sender being passed into the method, and it will be the button that was tapped. 1 * NSEC_PER_SEC means "delay for one second."

iOS XCode 6 NSTimer does not stopping by means of invalidate

Hi I would like to know what's is wrong with this codes :
IBOutlet UIImageView sam1;
IBOutlet UIImageView sam1;
NSTimer *timer1;
NSTimer *timer2;
-(void)start {
[self performSelector:#selector(myTimer1) withObj....
}
-(void)startAlso {
[self performSelector:#selector(myTimer2) withObj....
}
-(void)myTimer1 {
timer1 = [NSTimer scheduledTimeWithInterval .....
}
-(void)myTimer2 {
timer2 = [NSTimer scheduledTimeWithInterval .....
}
-(void)specialFuct {
[timer1 invalidate];
[timer2 invalidate];
}
Desc of my program is a simple game using imageview.
each imageview has their own NSTimer since they are both moving.
when 2 imageview collides, their timer is stop using invalidate.
my problem is, 2 imageview is coming from center.y -40 which is
outside the screen layout.
when the 2 imageview is inside the screen, it stops.
but when it is still outside the screen, -40 let say.
their NSTimer is still moving which is causing me a bug.
is their any explanation of why is this happening?
thank you in advance! first time xcoder here.
UPDATE!
I already found the problem,
it is on the
[self performSelector:#selector(myTimer1) withObj....
part.
What's happening is, the NSTimer is already shutdown but the performSelector delay is still runnning. My alternative way is to have a boolean and check if the game is already over or not.
if(gameOverIsNotOver) {
[self performSelector:#selector(myTimer1) withObject:nil afterDelay:3];
}
is there any best way to do this? thanks in advance!
You have awful description with dots in code. But hope my answer will help and solve your problem.
If you create scheduled NSTimer you can call invalidate to cancel its action.
For performSelector you can call:
[NSObject cancelPreviousPerformRequestsWithTarget:self];
And it will cancel all future selectors with delay for this (self) class.
Better don't use selectors and timers together. Global variables - is a bad pattern too.

Do something after a certain time after button clicked

I am trying to have an object disappear from the view after a certain time after a button is tapped. I'm a little confused with how to get the object to do something a certain after its tapped. I am not sure if I should use a run loop or NSTimer, and even if I know what to use Im still confused on what to do to make something happen a certain time after the button is tapped.
In your button pressed method you can use:
[self performSelector:#selector(myMethod) withObject:nil afterDelay:3];
And declare method with logic you want to run:
-(void) myMethod
{
//TODO: your logic goes here
}
You can even pass parameter to your method if you want (withObject argument).
You can use NStimer Also
NSTimer scheduledTimerWithTimeInterval:2.0
target:self
selector:#selector(afterTapped:)
userInfo:nil
repeats:NO];
and the create action to be done for afterTapped
-(void)afterTapped:(id)sender{
//do something
}

NSTimer Crash in ARC Project

I have the following pair of functions in a MessagePlayerViewController(UIViewController) which move a slider to reflect playback progress of an AVAudioPlayer:
-(void)startTrackingPlayback
{
if(!self.isPlaying)
{
self.isPlaying = YES;
self.playbackTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:#selector(updateProgress) userInfo:nil repeats:YES];
}
}
-(void)stopTrackingPlayback
{
if(self.playbackTimer)
{
if(self.playbackTimer.isValid)
{
[self.playbackTimer invalidate];
self.playbackTimer = nil;
}
}
self.isPlaying = NO;
}
Intermittently, and following no discernible pattern, I get an Exec Bad Access ith the top two items in the stack as:
0 objc_msgSend
1 [MessagePlayerViewController stopTrackingPlayback];
How can this be? I check if the timer exists before I call isValid and I check isValid before I invalidate it.
Using a breakpoint I can see that the timer does exist, but the error occurs when I set it to nil. If I remove this line, I get an identical error on the line:
[self.playbackTimer invalidate];
I would suggest inspecting the way you use your MessagePlayerViewController. It seems to me that both the stack trace and the behaviour you describe hint at the fact that it is the controller that is being deallocated earlier than your timer.
Take into account the fact that the run loop where the timer is scheduled will keep the timer alive.
Maybe the fix is as simple as calling invalidate in your controller's dealloc method (or somewhere else where it makes sense), but if you do not provide more code, it is not possible to say.

How do I execute my method when the UIView loads?

I have a view-based template app and have UILabel & UIButton. For debugging purposes I'm showing and hiding the button whilst changing the UILabel.text.
In C++ I would 'thread root();' to execute the root method but I don't know how to in Objective-c. How to run my 'root' method once the view loads?
-(void) root
{
[bombButton1 setHidden:NO];
int s = 0;
int j = 10;
while ( s < j )
{
[bombButton1 setHidden:YES];
NSString *debugLabelString = [NSString stringWithFormat:#"%d", s];
debugLabel.text=debugLabelString;
s++;
}
Edit:
Right, now I have: (but I get ERROR: Expected method body on the "-(void) rootMethod: NSTimer * timer {" line)
-(void) applicationDidFinishLaunching : (UIApplication *) application {
spawnTimer = [NSTimer scheduledTimerWithTimeInterval: 1.0 target:self selector:#selector(rootMethod:) userInfo:nil repeats: YES];
}
-(void) rootMethod: NSTimer * spawnTimer {
int s = 0;
int j = 10;
while ( s < j )
{
NSString *debugLabelString = [NSString stringWithFormat:#"%d", s];
debugLabel.text=debugLabelString;
//debugLabel.text=#"debug test complete";
s++;
}
}
Several ways to do this, I think. Here's one:
[self performSelectorInBackground:#selector(root) withObject:nil];
You'd make this call in say, your -(void)viewDidAppear: method.
You may run into issues running code on threads other than the main thread that tries to manipulate the UI.
That sleep(1) is worrisome. You could use a repeating NSTimer instead and eliminate the sleep(1) entirely. Something like:
[NSTimer scheduledTimerWithInterval:2.0 target:self
selector:#selector(root:) userInfo:nil repeats:YES];
For NSTimer, you'd have to change your method, root, to have a signature like
- (void)root:(NSTimer*)theTimer
You need to implement a called viewDidLoad.
- (void) viewDidLoad() {
// your code here
}
I'm sure you have your reasons, but you really shouldn't iteract with UI components in anything other than the UI thread. What you actually need to do is use an NSTimer to call a method on the UI thread multiple times.
What you should be doing is performSelectorOnMainThread when you want to update the UI Thread.
Do your running in the background and update variables that will contain the updated values, then use performSelectorOnMainThread on the View, sending it to a method that will merely update the Textbox with the data in the variables.
You can do anything in a background thread, except update the ui.
Edit: Furthermore I dont recommend using Timers in place of background threads, I have had instances when using Timers, where only so many get created and when I expected a background thread to fire, it never did. The timer actually never fired, even tho it was created.

Resources