Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Animation callback altering a different variable

I have a button which shows a view and which automatically goes away after specified time interval. Now if the button is pressed again while the view is already visible then it should go away and show a new view and the timer for new view be reset.

On the button press I have following code:

func showToast() {
    timer?.invalidate()
    timer = nil

    removeToast()

    var appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
    var toAddView = appDelegate.window!

    toastView = UIView(frame: CGRectMake(0, toAddView.frame.height, toAddView.frame.width, 48))
    toastView.backgroundColor = UIColor.darkGrayColor()
    toAddView.addSubview(toastView)

    timer = NSTimer.scheduledTimerWithTimeInterval(2.0, target: self, selector: Selector("removeToast"), userInfo: nil, repeats: false)

    UIView.animateWithDuration(0.5, animations: { () -> Void in
        self.toastView.frame.origin.y -= 48
    })
}

To remove toast i have the following code:

func removeToast() {
    if toastView != nil {
        UIView.animateWithDuration(0.5,
            animations: { () -> Void in
                self.toastView.frame.origin.y += 48
            },
            completion: {(completed: Bool) -> Void in
                self.toastView.removeFromSuperview()
                self.toastView = nil
            })
    }
}

Now even though I reset the timer each time by doing timer.invalidate() I get two calls in removeToast() which removes the newly inserted view. Can it be that UIView.animate be causing problems, I'm not getting how to debug the two callbacks for removeToast(). A demo project showing the behavior is here

NOTE: I did find some post saying to use dispatch_after() instead of timer, also as asked by @jervine10, but it does not suffice my scenario. As if I use dispatch_after then it's difficult to invalidate GCD call. Is there something that could be accomplished with NSTimers. I think that NSTimers are meant for this and there's something that I'm doing wrong.

like image 215
meteors Avatar asked Apr 20 '26 03:04

meteors


1 Answers

Sorry for not seeing your sample project, and thanks for directing me towards it. I can clearly see where the issue is, and the solution is super simple. Change remove toast to this:

func removeToast() {

    guard let toastView = self.toastView else {
        return
    }

    UIView.animateWithDuration(0.1,
        animations: { () -> Void in
            toastView.frame.origin.y += 48
        },
        completion: {(completed: Bool) -> Void in
            toastView.removeFromSuperview()

            if self.toastView == toastView {
                self.toastView = nil
            }
        })
}

Basically, the problem is that you are capturing self in the animation blocks, not toastView. So, once the animation blocks execute asynchronously, they will remove the new toastView set in the previous function.

The solution is simple and also fixes possible race conditions, and that is to capture the toastView in a variable. Finally, we check if the instance variable is equal to the view we are removing, we nullify it.

Tip: Consider using weak reference for toastView

like image 71
Mazyod Avatar answered Apr 22 '26 15:04

Mazyod



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!