Phil
Phil

Reputation: 36289

Unable to cancel NSTimer that executes blocks

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?

Upvotes: 1

Views: 301

Answers (2)

Phil
Phil

Reputation: 36289

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.

Upvotes: 2

Mohannad A. Hassan
Mohannad A. Hassan

Reputation: 1648

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.

Upvotes: 1

Related Questions