Animating Constraints

In this section, you are going to extend your animation to have the nextQuestionLabel property fly in from the left side of the screen and the currentQuestionLabel fly out to the right side of the screen when the user presses the Next Question button. In doing so, you will learn how to animate constraints.

First, you need a reference to the constraints that need to be modified. So far, all of your @IBOutlets have been to view objects. But outlets are not limited to views – in fact, any object in your interface file can have an outlet, including constraints.

At the top of ViewController.swift, declare two outlets for the two labels’ centering constraints.

@IBOutlet var currentQuestionLabel: UILabel!
@IBOutlet var currentQuestionLabelCenterXConstraint: NSLayoutConstraint!
@IBOutlet var nextQuestionLabel: UILabel!
@IBOutlet var nextQuestionLabelCenterXConstraint: NSLayoutConstraint!
@IBOutlet var answerLabel: UILabel!

Now open Main.storyboard. You want to connect these two outlets to their respective constraints. The easiest way to accomplish this is using the document outline. Click the disclosure triangle next to Constraints in the document outline and locate Current Question Label CenterX Constraint. Control-drag from the View Controller to that constraint (Figure 8.4) and select the correct outlet. Do the same for Next Question Label CenterX Constraint.

Figure 8.4  Connecting a constraint outlet

Screenshot shows the action of connecting a constraint to an outlet.

Currently, the Next Question button and the answer subviews have their center X constrained to the center X of the currentQuestionLabel. When you implement the animation for this label to slide offscreen, the other subviews will go with it. This is not what you want.

Select the constraint that centers the X value of the Next Question button to the currentQuestionLabel and delete it. Then Control-drag upward from the Next Question button to its superview and select Center Horizontally in Container.

Next, you want the two question labels to be one screen width apart. The center of nextQuestionLabel will be half of the screen width to the left of the view. The center of the currentQuestionLabel will be at its current position, centered in the screen.

When the animation is triggered, both labels will move a full screen width to the right, placing the nextQuestionLabel at the center of the screen and the currentQuestionLabel half a screen width to the right of the screen (Figure 8.5).

Figure 8.5  Sliding the labels

Two figure explains the placements of the Next and Current question labels before and after animation.

To accomplish this, when the view of ViewController is loaded, you need to move the nextQuestionLabel to its offscreen position.

In ViewController.swift, add a new method and call it from viewDidLoad().

func viewDidLoad() {
    super.viewDidLoad()
    currentQuestionLabel.text = questions[currentQuestionIndex]

    updateOffScreenLabel()
}

func updateOffScreenLabel() {
    let screenWidth = view.frame.width
    nextQuestionLabelCenterXConstraint.constant = -screenWidth
}

Now you want to animate the labels to go from left to right. Animating constraints is a bit different than animating other properties. If you modify the constant of a constraint within an animation block, no animation will occur. Why? After a constraint is modified, the system needs to recalculate the frames for all of the related views in the hierarchy to accommodate this change. It would be expensive for any constraint change to trigger this automatically. (Imagine if you updated quite a few constraints – you would not want it to recalculate the frames after each change.) So you must ask the system to recalculate the frames when you are done. To do this, you call the method layoutIfNeeded() on a view. This will force the view to lay out its subviews based on the latest constraints.

In ViewController.swift, update animateLabelTransitions() to change the constraint constants and then force the layout of the views.

func animateLabelTransitions() {

    // Animate the alpha
    // and the center X constraints
    let screenWidth = view.frame.width
    self.nextQuestionLabelCenterXConstraint.constant = 0
    self.currentQuestionLabelCenterXConstraint.constant += screenWidth

    UIView.animate(withDuration: 0.5,
        delay: 0,
        options: [],
        animations: {
            self.currentQuestionLabel.alpha = 0
            self.nextQuestionLabel.alpha = 1

            self.view.layoutIfNeeded()
        },
        completion: { _ in
            swap(&self.currentQuestionLabel,
                 &self.nextQuestionLabel)
    })
}

Finally, in the completion handler, you need to swap the two constraint outlets and reset the nextQuestionLabel to be on the left side of the screen.

func animateLabelTransitions() {

    // Animate the alpha
    // and the center X constraints
    let screenWidth = view.frame.width
    self.nextQuestionLabelCenterXConstraint.constant = 0
    self.currentQuestionLabelCenterXConstraint.constant += screenWidth

    UIView.animate(withDuration: 0.5,
        delay: 0,
        options: [],
        animations: {
            self.currentQuestionLabel.alpha = 0
            self.nextQuestionLabel.alpha = 1

            self.view.layoutIfNeeded()
        },
        completion: { _ in
            swap(&self.currentQuestionLabel,
                 &self.nextQuestionLabel)
            swap(&self.currentQuestionLabelCenterXConstraint,
                 &self.nextQuestionLabelCenterXConstraint)

            self.updateOffScreenLabel()
    })
}

Build and run the application. The animation works almost perfectly. The labels slide on and off the screen, and the alpha value animates appropriately as well.

There is one small problem to fix, but it can be a bit difficult to see. To see it more easily, turn on Slow Animations from the Debug menu in the simulator (Command-T). The width of all of the labels gets animated (to see this on the answerLabel, you need to click the Show Answer button). This is because the intrinsic content size changes when the text changes. The fix is to force the view to lay out its subviews before the animation begins. This will update the frames of all three labels to accommodate the next text before the alpha and sliding animations start.

Update animateLabelTransitions() to force the view to lay out its subviews before the animation begins.

func animateLabelTransitions() {

    // Force any outstanding layout changes to occur
    view.layoutIfNeeded()

    // Animate the alpha
    // and the center X constraints
    let screenWidth = view.frame.width
    self.nextQuestionLabelCenterXConstraint.constant = 0
    self.currentQuestionLabelCenterXConstraint.constant += screenWidth

    UIView.animate(withDuration: 0.5,
        delay: 0,
        options: [],
        animations: {
            self.currentQuestionLabel.alpha = 0
            self.nextQuestionLabel.alpha = 1

            self.view.layoutIfNeeded()
        },
        completion: { _ in
            swap(&self.currentQuestionLabel,
                 &self.nextQuestionLabel)
            swap(&self.currentQuestionLabelCenterXConstraint,
                 &self.nextQuestionLabelCenterXConstraint)

            self.updateOffScreenLabel()
    })
}

Build and run the application and cycle through some questions and answers. The minor animation issue is now resolved.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset