Search code examples
swiftuibuttonhidenstimer

Swift: NSTimer, delay hiding UIButton with each tap


I have a button (actually two nearly identical buttons) that unhide with an action in another UIView.

The buttons hide automatically 2 seconds after the panning in the second UIView ends using dispatch_after however I would like to keep the buttons visible if either is tapped while they are visible. Here is the timer property and two methods from the UIButton subclass. I have an @IBAction that calls "justTapped" in the ViewController.

var timer = NSTimer()

func hideAndShrink(){
    if !self.hidden && !timer.valid{
        self.hidden = true
    }
}

func justTapped(){
    timer.invalidate()
    timer = NSTimer.scheduledTimerWithTimeInterval(2.0, target: self, selector: "hideAndShrink", userInfo: nil, repeats: false)
}

Once either button is tapped, they will not hide.

Is the timer still valid while it calls the method in the selector?

The answer to this, thanks to luk2302, is Yes, the timer is valid when it sends the selector

How can I get around this?

As was suggested, I wrote a second method for the timer that hid the buttons without !timer.valid ?

It turned out that I did not need the second method at all as I just had any attempt to hide the buttons call justTapped instead. Here is the final code with expand and shrink animations for both buttons. self.direction is the direction I want the button to expand, from the bottom up, or the top down, and also determines the background image. I think it could have been an enumeration but I haven't figured those out yet.

    func unhideAndExpand(){

    if self.hidden{
        expand(self.frame.size.height)
    }

}

func hideAndShrink(){
    if !self.hidden && !shrinking{
        shrink(self.frame.size.height)
    }
}

func justTapped(){
    timer.invalidate()
    timer = NSTimer.scheduledTimerWithTimeInterval(3.0, target: self, selector: "hideAndShrink", userInfo: nil, repeats: false)
    }

func expand(height: CGFloat){

    if self.direction == "up"{
        self.transform = CGAffineTransformMake(1, 0, 0, 1/height, 1, height/2)
    } else if self.direction == "down" {
        self.transform = CGAffineTransformMake(1, 0, 0, 1/height, 1, -height/2)
    }
    self.hidden = false
    UIView.animateWithDuration(0.5, delay: 0, options: .CurveEaseOut, animations:
        {self.transform = CGAffineTransformIdentity}, completion: nil)
}

func shrink(height: CGFloat){
    self.shrinking = true
    UIView.animateWithDuration(0.5, delay: 0, options: .CurveEaseOut, animations:
        {if self.direction == "up"{
            self.transform = CGAffineTransformMake(1, 0, 0, 1/height, 1, height/2)
        } else if self.direction == "down" {
            self.transform = CGAffineTransformMake(1, 0, 0, 1/height, 1, -height/2)
            }
        }, completion: {_ in
            self.transform = CGAffineTransformIdentity
            self.hidden = true
            self.shrinking = false})
}

Thanks everyone for the help.


Solution

  • Is the timer still valid while it calls the method in the selector?

    Yes, it is

    The following is a complete runnable playground example demonstrating that:

    import Foundation
    import XCPlayground    
    XCPSetExecutionShouldContinueIndefinitely()
    
    class Bla {
        var timer : NSTimer?
    
        init() {
            timer = NSTimer.scheduledTimerWithTimeInterval(1, target: self, selector: "check", userInfo: nil, repeats: false)
            
            NSTimer.scheduledTimerWithTimeInterval(1.1, target: self, selector: "checkAgain", userInfo: nil, repeats: false)
        }
        
        @objc func check(){
            print(timer!.valid)
        }
        
        @objc func checkAgain(){
            check()
        }
    }
    
    let bla = Bla()
    

    This will first output true and then output false. The timer is still valid when firing. As soon as you leave the fired method the timer gets invalidated.

    That will render your if useless since the method will do nothing when fired by the timer because anything && !true evaluates to false.

    What you can do to circumvent that issue is create a different method timerFired which sets some internal property of your class, e.g. var timerHasFired = false to true. Your `` then has to check that variable instead of the timer:

    var timer = NSTimer()
    var timerHasFired = false
    
    func hideAndShrink(){
        if !self.hidden && hideAndShrink {
            self.hidden = true
        }
    }
    
    func timerFired() {
        timerHasFired = true
        hideAndShrink()
    }
    
    func justTapped(){
        timer.invalidate()
        timer = NSTimer.scheduledTimerWithTimeInterval(2.0, target: self, selector: "timerFired", userInfo: nil, repeats: false)
    }