Search code examples
iosswiftanimationuilabelupdating

Updating UILabels Off-Screen Causing Them To "Snap" Back


Background Info

I am building a tiny stopwatch app. It consists of a main view with a label displaying the time and two buttons: start & stop.
The start button activates NSTimers which then call methods that calculate the time passed and then update the labels accordingly. The stop button simply invalidates the timers.

On the upper right hand corner, there is an arrow button that is supposed to move all the UI elements to the left and show a menu. That works great when the timers and not running and thereby the labels are not being updated.

Issue

However, when running the timers & thereby updating the labels simultaneously, shortly after beginning the animation, everything snaps back to the position it was in previously. When invalidating the timers with the stop button, the animation is working fine again.

Demo

Demo of the issue

Code

Animation in toggleMenu()

UIView.animateWithDuration(0.5, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: 0.5, options: .CurveEaseOut, animations: {
    if self.arrow.frame.origin.x >= screenBounds.width/2 {
        //if menu is NOT visible (mainView ON-SCREEN, menu right)
        for element in self.UIElements {element.frame.origin.x -= screenBounds.width}
        self.arrow.frame.origin.x = 0
        self.arrow.transform = CGAffineTransformMakeRotation(CGFloat(-M_PI))
    } else {
        //if menu IS visible (mainView left, menu ON-SCREEN)
        for element in self.UIElements {element.frame.origin.x += screenBounds.width}
        self.arrow.frame.origin.x = screenBounds.width-self.arrow.frame.width
        self.arrow.transform = CGAffineTransformMakeRotation(CGFloat(0))
    }
    }, completion: nil)

Updating Labels in timer-called updateTime()

let currentTime = NSDate.timeIntervalSinceReferenceDate()
var elapsedTime: NSTimeInterval = currentTime - startTime

let hrs = UInt8(elapsedTime/3600)
elapsedTime -= NSTimeInterval(hrs)*3600
let mins = UInt8(elapsedTime/60)
elapsedTime -= NSTimeInterval(mins)*60
let secs = UInt8(elapsedTime)
elapsedTime -= NSTimeInterval(secs)

let strHrs  = String(format: "%02d", hrs)
let strMins = String(format: "%02d", mins)
let strSecs = String(format: "%02d", secs)

//update time labels
self.time.text = "\(strHrs):\(strMins)"
self.secs.text = "\(strSecs)"

Attempts

With no luck, I tried...

  • ...hiding the labels once the animation is finished
  • ...animating / hiding all the elements' superview once the animation is finished (clipsToBounds set true)

Note

I figured...

  • ...commenting out the lines where the labels are being updated "fixes" the issue
  • ...animating the UI elements to move only a couple pixels (like 100px) causes the same problem, meaning that the issue happens on- and off-screen


Any thoughts?
Thanks in advance!


Solution

  • When you update the labels, Auto Layout is running and placing your UI elements back to where their constraints say they should be. You shouldn't alter the frames of objects that are under the control of Auto Layout or you will get unexpected results.

    Instead of updating the origin.x of your UI elements, create @IBOutlets to the NSLayoutContraints that position them horizontally and then update the constant property of the constraints and call self.view.layoutIfNeeded() in your animation loop.

    It might be easier to make all of your UI elements be a subview of a top level UIView, and then you'd only need to update the one constraint that places that UIView horizontally to move it offscreen.